Skip to main content
X is retiring OAuth 1.0a user authentication. Apps have until January 31, 2027, to move to OAuth 2.0. After that date, requests signed with OAuth 1.0a stop working. Why: OAuth 2.0 is more secure for your users. OAuth 1.0a user tokens never expire, so a leaked token keeps working until the user revokes your app. OAuth 2.0 access tokens expire after 2 hours, refresh tokens are single-use, and each token carries only the scopes the user granted. Your users don’t need to do anything. You move existing users to OAuth 2.0 from your servers with a token exchange. There is no consent screen, no new login, and no notification. What changes in your integration: The work is mechanical: enable OAuth 2.0 on your App, run one batch job to exchange your stored tokens, change one request header, and add a refresh routine. Most teams can finish in under a week, and an AI coding agent can do most of it. See Migrate with an AI coding agent.
Exchange your users’ tokens before January 31, 2027. The exchange needs a working OAuth 1.0a token, so any user you haven’t migrated by then has to sign in again through the OAuth 2.0 flow.

Migration checklist

1

Enable OAuth 2.0 on your App

In the Developer Console, open the App your users authorized and enable OAuth 2.0 in its authentication settings. OAuth 1.0a keeps working: an App can have both enabled.
  • App type: for a server-side integration, choose Web App or Automated App / Bot. These are confidential clients and get a Client Secret. See OAuth 2.0 app types.
  • Callback URL: the exchange doesn’t use it, but users who have to sign in again through the OAuth 2.0 flow do.
  • Credentials: copy the Client ID and Client Secret from Keys and tokens. They are different from your OAuth 1.0a API key and secret.
Use the same App that issued your users’ OAuth 1.0a tokens. Tokens issued to a different App fail the exchange with invalid_grant.
2

Find your OAuth 1.0a code paths

List every place that signs requests with OAuth 1.0a, every place that stores or reads users’ OAuth 1.0a tokens, and every call to an X API v1.1 endpoint. OAuth 2.0 user tokens work with X API v2, so move v1.1 calls to their v2 equivalents. The endpoint map lists them. Ads API calls move to OAuth 2.0 too: see Ads API.
3

Add OAuth 2.0 token storage

For each user, store the access token, the refresh token, the access token’s expiry time, and the granted scopes. Keep the OAuth 1.0a token and secret until the retirement date: you need them to exchange again if a user’s OAuth 2.0 tokens are lost. Encrypt all tokens at rest and never log them.
4

Exchange your stored tokens

Run a batch job that sends one exchange request per user and stores the new tokens. Handle errors per user, stay within the rate limit, and don’t run two jobs over the same users at once. Your users see nothing.
5

Send requests with a Bearer token

Replace the OAuth 1.0a signature with Authorization: Bearer <access_token>. See Switch requests to Bearer tokens.
6

Refresh access tokens

Access tokens expire after 2 hours. Refresh them before they expire, and save the new refresh token from every response. See Refresh access tokens.
7

Test, then cut over

Migrate a few internal accounts first and compare results with your OAuth 1.0a path. Then switch users over in batches behind a flag you can turn off, and watch your error rates. Once all traffic uses OAuth 2.0, remove OAuth 1.0a signing from your code. After the retirement date, delete the stored OAuth 1.0a tokens.
Python and TypeScript code for steps 4 to 6 is in Code.

Migrate with an AI coding agent

Paste this prompt into an AI coding agent that has access to your codebase. It reads this guide as Markdown and makes the changes in small steps. Review the result like any other pull request.
Migration prompt

The exchange request

Make one HTTPS call per user to the same token endpoint you use for OAuth 2.0 refreshes:
Send the body as application/x-www-form-urlencoded with these parameters: Confidential clients authenticate with an Authorization: Basic header containing the base64-encoded <oauth2_client_id>:<oauth2_client_secret>. Public clients omit the header and include client_id=<oauth2_client_id> in the body.

Success response (HTTP 200)

Store the refresh_token and the current access_token against the user, mark the user migrated, then call the API with Authorization: Bearer <access_token> exactly as for any OAuth 2.0 user.
The user’s OAuth 1.0a token remains valid after the exchange. The migration is non-destructive: run it at your own pace until the retirement date.

What permissions do the new tokens get?

The OAuth 2.0 scopes are derived from what each user originally authorized under OAuth 1.0a, never more: Scopes not covered by the original OAuth 1.0a authorization (for example bookmark.read or space.read) are not granted. To gain those, send the user through the normal OAuth 2.0 authorization flow. See the scopes reference for what each scope allows.

Re-running and retries (rotation)

Exchanging the same OAuth 1.0a token again always works and returns a new OAuth 2.0 token pair, and invalidates the pair previously issued for that user and App. This makes migration jobs safe to re-run, but it means you must always store the most recent pair. X keeps one live OAuth 2.0 grant per user per App, so the same applies when a user signs in to your App again through the OAuth 2.0 flow.
Do not run two exchange jobs over the same users concurrently. Each exchange invalidates the token pair issued by the previous one.

Rate limits

The exchange is limited to 10,000 requests per 15 minutes per App per source IP address. A single worker migrating sequentially will rarely hit this; parallel workers should implement standard backoff on HTTP 429. At the full budget, one worker IP migrates roughly one million users per day.

Errors

Retrying an exchange that already succeeded is safe: you receive a fresh token pair, and the previous pair is invalidated (see Re-running and retries).
invalid_grant is deliberately generic. The response does not distinguish why a token was rejected.

Switch requests to Bearer tokens

On X API v2 endpoints, only the Authorization header changes. Endpoints, parameters, and responses stay the same.
With the Python or TypeScript XDK, pass the user’s access token instead of an OAuth1 object:
To see which scopes each endpoint needs, check the v2 authentication mapping.

Ads API

Exchanged tokens include ads.read, or ads.read and ads.write, when the user’s original authorization included Ads API permissions (see the permission mapping). The Ads API, including ads-api.x.com, accepts these OAuth 2.0 user tokens. Migrate your Ads API calls the same way: send Authorization: Bearer <access_token> instead of an OAuth 1.0a signature.

Refresh access tokens

Access tokens expire after 2 hours (expires_in is in seconds). Before one expires, get a new one from the same token endpoint with the refresh token:
The response contains a new access token and a new refresh token.
  • Refresh tokens are single-use. Each refresh returns a new refresh token, and the one you sent stops working. Save the new one before you use the new access token.
  • Refresh each user from one place at a time. If two workers refresh the same token at once, one of them fails. Use a per-user lock or a single refresh worker.
  • Refresh tokens last about 6 months. If one is lost or expires, exchange the user’s OAuth 1.0a token again while OAuth 1.0a is supported. After the retirement date, the user must sign in again through the OAuth 2.0 flow.
  • offline.access enables refresh. Exchanged tokens always include it. For users who sign in through the OAuth 2.0 flow, request offline.access, or you won’t get a refresh token.

Code

The XDKs don’t implement the token exchange grant, so this code calls the token endpoint directly. It handles confidential and public clients, the bulk exchange with the error handling above, and refresh with rotation. Replace TokenStore with your own database code.
Run migrate_users(store) (migrateUsers in TypeScript) as a batch job. Before each API call, get the user’s token with access_token_for(store, user_id) (accessTokenFor) and send it as shown in Switch requests to Bearer tokens. Other errors are raised as TokenError. For example, if the fallback exchange in access_token_for returns invalid_grant, mark the user as needing to sign in again.

FAQ

On January 31, 2027. Until then, OAuth 1.0a and OAuth 2.0 both work, so you can migrate at your own pace. After that date, requests signed with OAuth 1.0a fail and the token exchange is no longer available.
No. The exchange moves each user to OAuth 2.0 with no consent screen, no notification, and no session change. Their existing connection to your app continues. A user only has to sign in again if their OAuth 1.0a token is rejected with invalid_grant, if you need scopes their original authorization didn’t cover, or if you haven’t migrated them by the retirement date.
No. The OAuth 1.0a token remains valid until OAuth 1.0a is retired. v1.1 endpoints you call with OAuth 1.0a signing keep working until then.
Exchange the user’s OAuth 1.0a token again (while OAuth 1.0a remains supported) to get a fresh pair. After retirement, the user must re-authorize via the OAuth 2.0 flow.
No. They are ordinary OAuth 2.0 user tokens with the same refresh flow, the same scopes model, and the same revocation behavior.
OAuth 2.0 user tokens work with X API v2. Move v1.1 calls to their v2 equivalents before the retirement date. The endpoint map lists them.
All of them. Migrate in batches, monitor your error rates, and treat invalid_grant users as churn to re-acquire through the normal sign-in flow.
Questions about your migration? Contact your account manager, or ask on the developer forums.