Skip to main content
If your app has existing users who authorized it with OAuth 1.0a, you can migrate each of them to OAuth 2.0 without asking them to log in or re-authorize. The token exchange flow lets your servers trade a stored OAuth 1.0a access token and secret for an OAuth 2.0 access token and refresh token for the same user, with permissions matching what the user originally authorized.

Prerequisites

1

OAuth 2.0 enabled on your app

In the developer portal, open your App’s settings and make sure OAuth 2.0 is set up. You need your OAuth 2.0 Client ID and, for confidential clients, your Client Secret. These are different from your OAuth 1.0a consumer key and secret.
2

Your stored OAuth 1.0a credentials

For each user, the access token and its token secret, as issued when the user authorized your app.
3

A refresh-token loop

OAuth 2.0 access tokens expire after 2 hours. The exchange always returns a refresh token (valid for about 6 months and single-use; each refresh returns a new one). Your backend must store refresh tokens and refresh on expiry. If you already support OAuth 2.0 login, you have this. See the refresh token step in the OAuth 2.0 user access token guide for the refresh request.

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 current access_token) against the user, then call the API with Authorization: Bearer <access_token> exactly as for any OAuth 2.0 user.

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.

Migration walkthrough

For each stored OAuth 1.0a token:
1

Send the exchange request

POST /2/oauth2/token with the exchange parameters above.
2

On 200

Persist access_token and refresh_token for the user and mark the user migrated.
3

On 400 invalid_grant

The OAuth 1.0a token is no longer valid (the user revoked your app, changed relevant settings, or the token was already invalidated). Mark the user as needing normal OAuth 2.0 re-authorization if they return. Do not retry.
4

On 429 rate_limited

You have hit the exchange rate limit. Wait and retry with backoff (see Rate limits).
5

On 5xx

Transient. Retry with backoff. Retrying an already-successful exchange is safe: you simply receive a fresh token pair, and the previous pair is invalidated (see Re-running and retries).
6

Switch the user's traffic

Move the user’s API traffic to Authorization: Bearer and your standard OAuth 2.0 refresh loop.
Your OAuth 1.0a token for that user remains valid after the exchange. The migration is non-destructive. Run at your own pace; nothing forces a hard cutover until the OAuth 1.0a retirement date.

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 scripts safely re-runnable, but it means you should always persist the most recent pair.
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

invalid_grant is deliberately generic. The response does not distinguish why a token was rejected.

FAQ

No. There is no consent screen, no notification, and no session change. Their existing connection to your app continues.
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.
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.
Exchanged tokens carry ads.read and ads.write where the user’s original authorization included ads permissions, and these work on X API v2 ads endpoints. The legacy standalone Ads API (ads-api.x.com) continues to use OAuth 1.0a. Keep your OAuth 1.0a integration for it until further notice.
All of them, eventually, because OAuth 1.0a is being retired. Migrate in batches, monitor your error rates, and treat invalid_grant users as churn to re-acquire through the normal login flow.