Last updated: November 1, 2026
X retires OAuth 1.0a on January 1, 2027. Here is how to migrate.
After January 1, 2027, X API requests signed with OAuth 1.0a stop working. Before that date you can swap each existing token for an OAuth 2.0 pair with one POST to /2/oauth2/token, and users do not have to log in again. After it, the exchange is gone too, and every user you did not migrate has to authorize your app again.
The exchange is the easy part. The change that breaks bots is what comes after: an OAuth 1.0a token never expired, while an OAuth 2.0 access token lasts 2 hours and its refresh token works once. All details below come from X's token exchange guide, read November 1, 2026. X added the exchange to its changelog on September 21, 2026.
Are you affected?
You are if anything you run authenticates as a user with these four values: consumer key, consumer secret, access token and access token secret. Look for:
- Environment variables such as
X_ACCESS_TOKEN_SECRETorTWITTER_ACCESS_SECRET. - A request header that starts with
Authorization: OAuth oauth_consumer_key=. - Tweepy code using
OAuth1UserHandler, or aClientcreated withconsumer_keyandaccess_token_secret. - twitter-api-v2 (Node) created with
appKey,appSecret,accessTokenandaccessSecret. - An automation tool or agent plugin that asked you to paste those four keys. Check whether it has an OAuth 2.0 option before the deadline.
An app-only Bearer token is a different kind of auth and is not what this retirement covers. Accounts connected through a third-party tool are that tool's job to migrate, not yours.
Step 1: exchange the token
Enable OAuth 2.0 on the app in the Developer Console and copy the client ID and secret. Then send the existing token and secret:
# Confidential client. A public client omits -u and sends client_id in the body.
curl -X POST https://api.x.com/2/oauth2/token \
-u "$X_CLIENT_ID:$X_CLIENT_SECRET" \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "grant_type=urn:ietf:params:oauth:grant-type:token-exchange" \
--data-urlencode "subject_token=$X_ACCESS_TOKEN" \
--data-urlencode "subject_token_type=urn:x:params:oauth:token-type:oauth1_token" \
--data-urlencode "oauth_token_secret=$X_ACCESS_TOKEN_SECRET"A successful call returns HTTP 200:
{
"token_type": "bearer",
"expires_in": 7200,
"access_token": "...",
"refresh_token": "...",
"scope": "tweet.read users.read follows.read ... offline.access"
}Save both tokens right away. You can pass an optional scope with a space-separated subset of the mapped scopes if you want a narrower token, which is worth doing for a bot that only posts.
Run it once, per user, from one job
Exchanging the same OAuth 1.0a token again returns a new pair and invalidates the previous one. If two jobs cover the same users, or you re-run a job without keeping its output, the tokens you stored stop working. For many users, the limit is 10,000 requests per 15 minutes, per App, per source IP.What the new token can do
Scopes follow the permission the OAuth 1.0a token already had. If your token was read-only, the exchange will not make it able to post. Those users have to authorize again.
Step 2: handle refresh before you switch
A cron job that has posted with the same four keys for years now needs writable storage. The access token expires after 2 hours, and each refresh returns a new refresh token while the one you sent stops working. If two workers refresh at the same time, X says one of them fails.
import json, os, requests
TOKEN_FILE = "x_tokens.json"
def refresh():
with open(TOKEN_FILE) as f:
tokens = json.load(f)
r = requests.post(
"https://api.x.com/2/oauth2/token",
auth=(os.environ["X_CLIENT_ID"], os.environ["X_CLIENT_SECRET"]),
data={"grant_type": "refresh_token", "refresh_token": tokens["refresh_token"]},
timeout=30,
)
r.raise_for_status()
new_tokens = r.json()
# Write the new pair before using it. The old refresh token is already dead.
tmp = TOKEN_FILE + ".tmp"
with open(tmp, "w") as f:
json.dump(new_tokens, f)
os.replace(tmp, TOKEN_FILE)
return new_tokens["access_token"]Keep refresh in one place, behind a lock if you have several workers. A read-only filesystem or a secret you cannot update at runtime will not work as token storage, and a GitHub Actions secret needs an extra step to write back. More on the failure modes in Fix X OAuth refresh token expired.
Step 3: switch and test
- Replace the OAuth 1.0a signature with
Authorization: Bearer ACCESS_TOKEN. - Test every write you use: a plain post, a post with media, and anything else your app does. Media needs the
media.writescope. - Leave the bot running for more than 2 hours and confirm it refreshes by itself. This is the test people skip.
- Delete the OAuth 1.0a credentials once nothing uses them.
The OAuth 1.0a token stays valid after the exchange, so you can roll back until the retirement date. Do not leave this for the last week of December.
Exchange errors
Or stop maintaining the auth
If your app only posts, you may not want token storage, refresh locks and an X bill to look after. With OpenTweet you log in to X once and call one endpoint with a key that does not expire every 2 hours:
curl -X POST https://opentweet.io/api/v1/posts \
-H "Authorization: Bearer ot_your_key" \
-H "Content-Type: application/json" \
-d '{"text": "Still posting after January 1.", "scheduled_date": "2027-01-04T15:00:00Z"}'You lose direct control of the X app, and it costs $19.99 a month, or $29 with 10 link posts a day, with X API fees included. For a large app with its own users, migrating is the right call. For one bot or a few accounts, compare it against what you pay now on the X API.
7-day free trial with a few limits, or pay now for full limits.
Frequently asked questions
When does X retire OAuth 1.0a?
January 1, 2027. X's token exchange guide says requests signed with OAuth 1.0a stop working after that date. The token exchange itself is no longer available after that date either.
Do my users have to log in again?
Not if you exchange their tokens before the deadline. POST /2/oauth2/token accepts the OAuth 1.0a token and secret and returns an OAuth 2.0 token pair with no re-authorization. After January 1, 2027 the exchange is gone, so anyone you did not migrate has to sign in through OAuth 2.0.
Does the OAuth 1.0a token keep working after I exchange it?
Yes, until the retirement date. X says the OAuth 1.0a token stays valid after the exchange, so you can run the migration first and switch your signing code later.
What happens if I run the exchange twice for the same user?
It works and returns a new OAuth 2.0 pair, and the pair issued before it stops working. Do not run two exchange jobs over the same users at once, and do not re-run a job without saving what the last run returned.
How long do the new tokens last?
The access token lasts 2 hours (expires_in 7200). X describes the refresh token as lasting about 6 months and single-use: each refresh returns a new one and the one you sent stops working. A bot that used to run on a token that never expired now has to store a new refresh token every time.
Which scopes do I get after the exchange?
Only what the OAuth 1.0a permission covered, plus offline.access. A "Read and write" app gets tweet.write and media.write among others. Direct Message scopes only come from the "Read, write, and Direct Messages" permission. The exchange cannot give a token more than it had.
What is the exchange rate limit?
10,000 requests per 15 minutes, per App, per source IP address. Back off and retry on 429 rate_limited.
Does OpenTweet use OAuth 1.0a? Do I need to do anything?
No. OpenTweet connects X accounts with OAuth 2.0 and you call it with an ot_ API key, so there is nothing to migrate on your side. It is also an option if you would rather drop your own developer app than migrate it: $19.99/mo, or $29/mo with 10 link posts a day, X API fees included.
Keep exploring
X API auth, errors and posting without your own developer app.
Fix X OAuth refresh token expired
The single-use rotation and race condition, with Node and Python code.
Upload media with the X API
The v2 upload endpoints and the media.write scope.
Why does POST /2/tweets return 403?
The causes of X API 403 errors when posting, and the fixes.
Post to X without a developer account
One API key and a normal X login, with no app of your own.
X API pricing
What posts, link posts and reads cost on pay-per-use.
X API error decoder
Paste an X API error and see what it means.
Keep posting after January 1
Connect your X account with a normal login and post from any script or AI agent. No developer app and no tokens to refresh.
7-day free trial with a few limits, or pay now for full limits.