Twitter API Access Token: Bearer, OAuth 1.0a, OAuth 2.0
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
403with error code220, "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:
- Get a request token.
POST https://api.x.com/oauth/request_tokenwith a URL-encodedoauth_callbackthat's registered in your app settings. Check thatoauth_callback_confirmedistrue. - 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 withoauth_tokenandoauth_verifier. - Exchange for an access token.
POST https://api.x.com/oauth/access_tokenwith theoauth_verifier. The response has the user'soauth_tokenandoauth_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:
- Build the authorize URL.
https://x.com/i/oauth2/authorizewithresponse_type=code, yourclient_id, an exact-matchredirect_uri, thescopelist, a randomstate, acode_challenge, andcode_challenge_method=S256. Addoffline.accessto the scopes, or you get no refresh token. - Verify
stateon the redirect. X returnscodeandstateto your redirect URI. - 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.accessis 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_grantmeans 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, andoffline.access. - Partial consent is stored as granted. If a feature needs a scope the user didn't grant, the call returns
403 permission_requiredwith 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.