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

How to get a Twitter (X) API access token: the app-only Bearer Token, OAuth 1.0a consumer key and access token, and OAuth 2.0 PKCE with refresh tokens.

Canonical page: https://www.tryadeli.com/blog/x-api-access-token
Last updated: 2026-10-26

By Nick Reyes, Co-founder and CTO, Adeli. Published 2026-10-26.

## 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](https://www.tryadeli.com/blog/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](https://docs.x.com/resources/fundamentals/authentication/overview), 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](https://docs.x.com/resources/fundamentals/authentication/guides/v2-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](https://docs.x.com/resources/fundamentals/authentication/oauth-2-0/application-only)).

**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](https://www.tryadeli.com/blog/x-api-rate-limit).

## 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](https://docs.x.com/resources/fundamentals/authentication/oauth-2-0/bearer-tokens):

```bash
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:

```bash
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](https://docs.x.com/resources/fundamentals/authentication/oauth-1-0a/obtaining-user-access-tokens):

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](https://docs.x.com/fundamentals/authentication/oauth-2-0/oauth-1-0a-token-exchange) 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](https://docs.x.com/resources/fundamentals/authentication/oauth-2-0/authorization-code):

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.

```bash
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?](https://developer.okta.com/blog/2019/05/01/is-the-oauth-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.**

```bash
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](https://docs.x.com/resources/fundamentals/authentication/oauth-2-0/authorization-code) |
| **OAuth 2.0 access token** | 2 hours (`expires_in: 7200`) | [Token exchange](https://docs.x.com/fundamentals/authentication/oauth-2-0/oauth-1-0a-token-exchange) |
| **OAuth 2.0 refresh token** | About 6 months, **single-use** | [Token exchange](https://docs.x.com/fundamentals/authentication/oauth-2-0/oauth-1-0a-token-exchange) |
| **OAuth 1.0a access token** | No expiry; revocable | [3-legged OAuth](https://docs.x.com/resources/fundamentals/authentication/oauth-1-0a/obtaining-user-access-tokens) |

**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.

```bash
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](https://docs.x.com/resources/fundamentals/authentication/oauth-2-0/application-only), [error reference](https://docs.x.com/x-api/fundamentals/response-codes-and-errors), and [token exchange](https://docs.x.com/fundamentals/authentication/oauth-2-0/oauth-1-0a-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](https://www.tryadeli.com/product/x) 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](https://www.tryadeli.com/docs/api/connect):

- **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](https://www.tryadeli.com/social-media-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](https://www.tryadeli.com/use-cases/vertical-saas).

## 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.

## Related Adeli pages

- [X (Twitter) API for Developers](https://www.tryadeli.com/product/x.md): Publish, schedule, and read analytics for X (Twitter) through one API. Adeli owns the X developer app, token refresh, and review process. Free to start.
- [Social Media Posting & Scheduling API](https://www.tryadeli.com/social-media-posting-api.md): Adeli's social media posting and scheduling API publishes to Instagram, TikTok, YouTube, Facebook, X, and Bluesky in one request, now or at a set time. Free to start.
- [Social Media API for Vertical SaaS Platforms](https://www.tryadeli.com/use-cases/vertical-saas.md): Give your small-business customers posting, scheduling, analytics, inbox, and Meta ads inside your vertical SaaS, through one API.
- [All posts on the Adeli blog](https://www.tryadeli.com/blog.md)

## About the author

Nick Reyes is the CTO behind Adeli and builds its API, its platform integrations, and the docs. He is a product engineer with 10 years of experience at Phantom, Airbnb, and Google.
