Twitter API Access Token: Bearer, OAuth 1.0a, OAuth 2.0

By Nick ReyesPublished 7 min read

The short answer

The Twitter API, now the X API, has three kinds of access token. The app-only Bearer Token reads public data. OAuth 1.0a gives a user access token and secret that don't expire. OAuth 2.0 with PKCE gives a user access token that lasts 2 hours plus a refresh token if you request offline.access. Posting needs a user token. Adeli holds the X app and refreshes every account's tokens.

A Twitter API access token is the credential that lets your app call the X API (formerly the Twitter API). There are three kinds, and which one you need depends on the endpoint: the app-only Bearer Token for public reads, an OAuth 1.0a access token and secret for acting as a user with signed requests, and an OAuth 2.0 user access token from the PKCE flow for acting as a user with scopes. Only the two user tokens can post.

Signing up for the Developer Console and creating an app is covered in X API access. This post starts once you have an app: how to get each token, how long it lasts, how to refresh or revoke it, and what the errors mean. Every X fact links to X's authentication docs, checked on 2026-10-26.

Which X API access token do I need?

Use the Bearer Token for public data and a user access token for anything private or anything that writes. X's authentication mapping lists which methods each v2 endpoint accepts:

Endpoint Bearer Token (app-only) OAuth 1.0a user OAuth 2.0 PKCE user, scopes
POST /2/tweets (post) No Yes Yes: tweet.read, tweet.write, users.read
GET /2/tweets/search/recent Yes Yes Yes: tweet.read, users.read
GET /2/tweets/search/all Yes No No
GET /2/users/:id/tweets Yes Yes Yes: tweet.read, users.read
PUT /2/tweets/:id/hidden No Yes Yes: adds tweet.moderate.write
GET /2/users/:id/bookmarks No No Yes: adds bookmark.read

The app-only guide spells out what the Bearer Token can't do: post, access Direct Messages or account credentials, or read users' email addresses (app-only auth).

Rate limits follow the token. User tokens count against per-user limits and the Bearer Token against per-app limits, from separate pools. Limits per endpoint are in X API rate limits.

How to get an X API Bearer Token

Copy the Bearer Token from your app's Keys and tokens page, or generate it with your API Key and Secret. The Bearer Token is X's name for the app-only access token, based on OAuth 2.0 client credentials. To generate it yourself, per X's Bearer Token guide:

curl -u "$API_KEY:$API_SECRET" \
  --data 'grant_type=client_credentials' \
  'https://api.x.com/oauth2/token'

The response is {"token_type":"bearer","access_token":"AAAA..."}. Check that token_type is bearer, then send the token on every request:

curl "https://api.x.com/2/tweets/search/recent?query=from:XDevelopers" \
  -H "Authorization: Bearer $BEARER_TOKEN"

Three things to know:

  • One per app. Asking again with the same credentials returns the same token until you invalidate it with POST oauth2/invalidate_token.
  • Treat it like a password. X says the consumer key and secret and the Bearer Token all grant access as your app.
  • It's read-only and public-only. Calling an endpoint that needs a user returns 403 with error code 220, "Your credentials do not allow access to this resource."

How to get an OAuth 1.0a access token and secret

For your own account, generate the Access Token and Secret in the console. For anyone else, run the 3-legged OAuth flow. OAuth 1.0a needs four values on every request: your consumer key and secret (the API Key and Secret) and the user's access token and secret. X's terminology guide treats "API Key", "Consumer Key", and oauth_consumer_key as the same thing.

The 3-legged flow, from X's user access token guide:

  1. Get a request token. POST https://api.x.com/oauth/request_token with a URL-encoded oauth_callback that's registered in your app settings. Check that oauth_callback_confirmed is true.
  2. Send the user to X. Redirect to https://api.x.com/oauth/authorize?oauth_token=<request token>. X sends the user back to your callback with oauth_token and oauth_verifier.
  3. Exchange for an access token. POST https://api.x.com/oauth/access_token with the oauth_verifier. The response has the user's oauth_token and oauth_token_secret.

These tokens don't expire, but the user can revoke them at any time. Each request has to be signed (X's examples use HMAC-SHA1), and X recommends a tool or library over building the signature by hand.

OAuth 1.0a is on its way out. X's token exchange guide says OAuth 1.0a "is being retired" and tells apps to migrate all their users. The guide doesn't give a date. New integrations should start on OAuth 2.0.

How to get an OAuth 2.0 user access token with PKCE

Turn on OAuth 2.0 in your app settings, send the user to X's authorize URL, and exchange the code for tokens within 30 seconds. OAuth 2.0 works with the X API v2 only. The steps, from X's OAuth 2.0 guide:

  1. Build the authorize URL. https://x.com/i/oauth2/authorize with response_type=code, your client_id, an exact-match redirect_uri, the scope list, a random state, a code_challenge, and code_challenge_method=S256. Add offline.access to the scopes, or you get no refresh token.
  2. Verify state on the redirect. X returns code and state to your redirect URI.
  3. Exchange the code fast. The authorization code expires 30 seconds after the user approves.
curl -X POST 'https://api.x.com/2/oauth2/token' \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -u "$CLIENT_ID:$CLIENT_SECRET" \
  --data-urlencode "code=$CODE" \
  --data-urlencode 'grant_type=authorization_code' \
  --data-urlencode "redirect_uri=$REDIRECT_URI" \
  --data-urlencode "code_verifier=$CODE_VERIFIER"

That's the confidential-client form (Web App, Automated App or bot). Public clients (Native App, Single page App) have no secret: drop -u and send client_id in the body instead.

The code_verifier is what makes a stolen code useless. Aaron Parecki, who writes and teaches on OAuth, put it this way in Is the OAuth 2.0 Implicit Flow Dead? (May 1, 2019):

"The Authorization Code flow with PKCE adds an additional step which allows us to protect the authorization code so that even if it is stolen during the redirect it will be useless by itself."

Generate a fresh random verifier for every login and derive the challenge with S256. X's examples use plain to stay readable; production code shouldn't.

How to refresh an X OAuth 2.0 access token

POST the refresh token to https://api.x.com/2/oauth2/token with grant_type=refresh_token, and save the new refresh token that comes back.

curl -X POST 'https://api.x.com/2/oauth2/token' \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -u "$CLIENT_ID:$CLIENT_SECRET" \
  --data-urlencode "refresh_token=$REFRESH_TOKEN" \
  --data-urlencode 'grant_type=refresh_token'

The lifetimes that shape your refresh job:

Token Lifetime Source
Authorization code 30 seconds OAuth 2.0 PKCE
OAuth 2.0 access token 2 hours (expires_in: 7200) Token exchange
OAuth 2.0 refresh token About 6 months, single-use Token exchange
OAuth 1.0a access token No expiry; revocable 3-legged OAuth

Single-use is where refresh jobs break. If two workers refresh the same account at once, the first succeeds and the second sends a refresh token that's already spent. Lock per account, write the new pair before releasing the lock, and refresh a few minutes before the 2 hours are up. Even then, handle a 401 on any call, because the user can revoke your app at any time.

To log a user out, revoke the token with POST https://api.x.com/2/oauth2/revoke. Users can also revoke your app from X's connected apps settings.

How to migrate OAuth 1.0a tokens to OAuth 2.0

Exchange each stored OAuth 1.0a token and secret at POST /2/oauth2/token with the token-exchange grant. X added this on September 21, 2026. The user sees no consent screen.

curl -X POST 'https://api.x.com/2/oauth2/token' \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -u "$CLIENT_ID:$CLIENT_SECRET" \
  --data-urlencode 'grant_type=urn:ietf:params:oauth:grant-type:token-exchange' \
  --data-urlencode "subject_token=$OAUTH1_ACCESS_TOKEN" \
  --data-urlencode 'subject_token_type=urn:x:params:oauth:token-type:oauth1_token' \
  --data-urlencode "oauth_token_secret=$OAUTH1_TOKEN_SECRET"
  • Scopes follow the old permission. A "Read and write" app gets tweet.write, media.write, and the other write scopes. offline.access is always included.
  • The old token keeps working, so you can migrate in batches.
  • Don't run two exchange jobs at once. Each exchange invalidates the pair the previous one issued.
  • 400 invalid_grant means the user revoked your app. Don't retry; send them through the OAuth 2.0 login next time.

What do X API token errors mean?

Most token errors come down to the wrong token type, an expired token, or a regenerated credential.

Response Meaning Fix
401, code 89, "Invalid or expired token" Token wrong, revoked, or past its 2 hours Refresh (OAuth 2.0) or re-authorize
403, code 220 Bearer Token on an endpoint that needs a user Use a user access token
403, code 99 on oauth2/token Bad app credentials, or requesting Bearer Tokens too often Check the API Key and Secret
401 after a console change Keys or tokens were regenerated Update the stored values everywhere
400 invalid_grant on exchange OAuth 1.0a token no longer valid Re-authorize with OAuth 2.0

Sources: app-only auth, error reference, and token exchange.

Is there a faster way to manage X access tokens?

Yes, if your customers connect their own X accounts. Every connected account means a PKCE login, a 2-hour access token, a single-use refresh token, and a job that keeps it all current without racing itself.

Adeli's X API maintains the X developer app, has passed X's review, and refreshes every connected account's tokens, so your product holds one Adeli API key and never stores an X token. From the connect docs:

  • Adeli runs the OAuth 2.0 flow through its X app and requests tweet.read, tweet.write, users.read, media.write, and offline.access.
  • Partial consent is stored as granted. If a feature needs a scope the user didn't grant, the call returns 403 permission_required with guidance to restart OAuth.
  • Reconnecting the same X account keeps the same accountId, so your records don't change.
  • X's API usage fees are passed through at X's published rates. Until your workspace has a card on file, starting an X connect returns 402 x_billing_required.

The same posting API publishes to Instagram, TikTok, YouTube, Facebook, and Bluesky. For vertical SaaS products adding social features for their customers, see Adeli for vertical SaaS.

faq

Frequently asked questions

How do I get a Twitter consumer key and access token?

Create an app in the X Developer Console at console.x.com. The console shows the API Key and Secret, which are the consumer key and secret, once at creation. On the app's Keys and tokens tab you can also generate an Access Token and Secret for your own account. Store all four right away; regenerating one invalidates the old value.

Does the X API Bearer Token expire?

X doesn't give the app-only Bearer Token an expiry. One Bearer Token is valid per app at a time, and requesting a new one with the same credentials returns the same token until you invalidate it with POST oauth2/invalidate_token or regenerate it in the console.

How long does an X OAuth 2.0 refresh token last?

X's token exchange guide says an OAuth 2.0 refresh token is valid for about 6 months and is single-use: each refresh returns a new refresh token, and you must store the new one. The access token it produces lasts 2 hours.

Why does my X API token return 401 'Invalid or expired token'?

X returns 401 with error code 89 when a token is wrong, revoked, or expired. For OAuth 2.0, the access token has passed its 2-hour life, so refresh it. For a Bearer Token or OAuth 1.0a tokens, check whether they were regenerated in the console or the user revoked your app.

Power your next project
with one ads & social media API

Give your users publishing, scheduling, and analytics through one integration your team can actually maintain.

Try with agents

Start with free credits. No credit card required.