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.
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.
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.
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: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)
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.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 theAuthorization header changes. Endpoints, parameters, and responses stay the same.
OAuth1 object:
Ads API
Exchanged tokens includeads.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:
- 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.accessenables refresh. Exchanged tokens always include it. For users who sign in through the OAuth 2.0 flow, requestoffline.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. ReplaceTokenStore with your own database code.
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
When does OAuth 1.0a stop working?
When does OAuth 1.0a stop working?
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.
Does the exchange log users out or break my OAuth 1.0a integration?
Does the exchange log users out or break my OAuth 1.0a integration?
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.
What if I lose a refresh token, or it expires after 6 months?
What if I lose a refresh token, or it expires after 6 months?
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.
Do exchanged tokens behave differently from normal OAuth 2.0 tokens?
Do exchanged tokens behave differently from normal OAuth 2.0 tokens?
No. They are ordinary OAuth 2.0 user tokens with the same refresh flow, the same scopes model, and the same revocation behavior.
What about v1.1 endpoints?
What about v1.1 endpoints?
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.
Which users should I migrate?
Which users should I migrate?
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.