X API OAuth 2.0 with PKCE: Scopes, Tokens, Python (2026)
How X API OAuth 2.0 works in 2026: the PKCE authorize and token URLs, exact scopes, 2-hour tokens and refresh, callback rules, and a tested Python flow.
Tarun Khatri
Published · Updated · 6 min read
X API OAuth 2.0 means Authorization Code with PKCE: a user approves your app on X, and you swap the code for a token.
This is for developers whose app acts as an X (Twitter) user, to post or read bookmarks. You get X's URLs, scopes and token rules, plus a Python flow we ran against X's token endpoint on 24 October 2026.
The URL you send users to:
https://x.com/i/oauth2/authorize?response_type=code&client_id=YOUR_CLIENT_ID&redirect_uri=YOUR_CALLBACK&scope=tweet.read%20users.read%20offline.access&state=RANDOM&code_challenge=CHALLENGE&code_challenge_method=S256
- Turn on OAuth 2.0Pick an app type and add your callback URL.
- Build the authorize URLHash a verifier into an S256 challenge.
- Swap the codePOST it to the token endpoint, fast.
- RefreshTrade the refresh token for a new pair.
OAuth 1.0a vs OAuth 2.0: which X API auth do you need?
It depends on whose data you touch. We counted the auth rules on the 240 operations that need auth in X's OpenAPI file, version 2.172:
| What the call accepts | Operations | Examples |
|---|---|---|
| App-only bearer only | 44 | Filtered stream, compliance jobs |
| Bearer or any user token | 36 | Recent search, user lookup |
| Bearer or OAuth 2.0 user | 8 | Spaces, trends |
| OAuth 1.0a or OAuth 2.0 | 143 | Post, like, DMs, /2/users/me |
| OAuth 2.0 user token only | 9 | Add a bookmark, create a folder |
So reading public posts never needs a user. Writing and private data do.
- OAuth 1.0a signs every request with four keys. X's 3-legged guide says its user tokens "do not expire."
- OAuth 2.0 with PKCE shows the user named scopes and works with the v2 API only.
- Already on 1.0a? Since 21 September 2026, X's token exchange turns stored 1.0a tokens into 2.0 tokens without a new sign-in.
Step 1: Turn on OAuth 2.0 and register the callback
Open your app at console.x.com and turn on OAuth 2.0 in its authentication settings. A Client ID appears under Keys and Tokens. Our guide to getting an X API key covers creating the app.
Then pick an app type:
- Confidential: Web App, and Automated App or bot. You get a Client Secret.
- Public: Native App and Single Page App. No secret, so PKCE is the only proof.
X's apps page says the callback URL must match exactly, trailing slash included, with up to 10 per app. Use https:// in production and http://127.0.0.1, not localhost, for local work.
X's own XDK and xurl examples still use localhost:8080, so X's pages disagree. Register the exact string your code sends.
Step 2: Build the X API OAuth 2.0 PKCE authorize URL
PKCE means you keep a secret verifier and send only its SHA-256 hash. Standard library only:
import base64, hashlib, os, secrets
from urllib.parse import urlencode, quote
CLIENT_ID = os.environ["X_CLIENT_ID"]
REDIRECT_URI = "http://127.0.0.1:3000/callback" # exactly as registered
verifier = secrets.token_urlsafe(64) # 86 characters, stays on your server
digest = hashlib.sha256(verifier.encode()).digest()
challenge = base64.urlsafe_b64encode(digest).rstrip(b"=").decode()
state = secrets.token_urlsafe(16) # check it when X sends the user back
params = {"response_type": "code", "client_id": CLIENT_ID,
"redirect_uri": REDIRECT_URI, "state": state,
"scope": "tweet.read users.read offline.access",
"code_challenge": challenge, "code_challenge_method": "S256"}
print("https://x.com/i/oauth2/authorize?" + urlencode(params, quote_via=quote))
Our run printed this, trimmed:
https://x.com/i/oauth2/authorize?response_type=code&client_id=YOUR_CLIENT_ID&redirect_uri=http%3A%2F%2F127.0.0.1%3A3000%2Fcallback&state=s_GrsGQ...&scope=tweet.read%20users.read%20offline.access&code_challenge=tAEaNSKGViFlDKS-kKt3HZhKxVeBGtYQvBrDuHnMF_Q&code_challenge_method=S256
- Use S256. X also takes
plain, and its examples show it, but then the secret travels in the URL. - The hash is right. Our code gave RFC 7636's own test answer for its sample verifier.
- Mind the host. X's OpenAPI file lists
api.x.com/2/oauth2/authorize, but our GET there got error 215. Use thex.comone.
Step 3: Swap the code for tokens
X sends the user to your callback with code and state. Check state, then post the code with the same verifier:
import requests
TOKEN_URL = "https://api.x.com/2/oauth2/token"
def exchange(code):
r = requests.post(TOKEN_URL, data={
"code": code, "grant_type": "authorization_code",
"client_id": CLIENT_ID, "redirect_uri": REDIRECT_URI,
"code_verifier": verifier,
}, timeout=30)
print(r.status_code, r.json())
r.raise_for_status()
return r.json() # access_token, expires_in, refresh_token
A confidential client adds auth=(CLIENT_ID, CLIENT_SECRET), which sends the Basic header X asks for.
We called exchange("not-a-real-code") with a fake Client ID. X answered:
400 {'error': 'invalid_client', 'error_description': 'Value passed for the client id was invalid.'}
What we didn't run: a real sign-in, code swap, refresh or user call. Those need an X app with credits.
Step 4: Refresh tokens before two hours pass
X's PKCE page says the access token lasts two hours. You get a refresh token only if you asked for offline.access.
The last line is from X's token-exchange page. Each refresh returns a new refresh token, so save it.
def refresh(refresh_token):
r = requests.post(TOKEN_URL, data={
"grant_type": "refresh_token", "refresh_token": refresh_token,
"client_id": CLIENT_ID,
}, timeout=30)
r.raise_for_status()
return r.json() # save the new refresh_token: the old one is spent
With fake values, it got the same 400. To sign a user out, post the token to https://api.x.com/2/oauth2/revoke.
Which X API OAuth 2.0 scopes do you need?
Ask for the fewest your calls need. X's spec names them per endpoint:
tweet.readandusers.read: nearly every user call, evenGET /2/users/me.tweet.write: posting, with those two. A post costs $0.015 on X's pricing page.offline.access: the refresh token.bookmark.read,like.read,follows.read,list.read,dm.read: private reads, each with a.writetwin.media.write: media uploads.
X's docs table lists 24 scopes. Its OpenAPI file declares 34, with newer ones such as settings.read.
How Weir's MCP server does OAuth for AI apps
Chat apps can't sign in to X's OAuth on their own: X's MCP docs say it "requires your own developer app." Weir's MCP server uses the same code-plus-PKCE shape, built so a client needs no app:
- The client registers itself, by dynamic client registration or a Client ID Metadata Document.
- Every client is public: S256 only, no secret.
- Tokens last one hour and refresh by rotation.
The token is a Weir token. Weir reads public data only and never acts as you on X. We run Weir, so weigh that.
If X's OAuth 2.0 flow fails
- 415, "Callback URL not approved": your
redirect_uridiffers from the console, even by a slash. - 400
invalid_client: the Client ID is wrong. unauthorized_client, "Missing valid authorization header": a confidential client skipped the Basic header, per a note in xurl's source.- The code expired: you swapped it after 30 seconds.
- No
refresh_tokencame back: addoffline.accessand sign in again. - 403 with a bearer token: that endpoint needs a user. Our X API error codes guide lists the rest.
When not to use Weir
- You post, like, DM or read bookmarks. That needs a user token on X's API. Weir is read-only.
- X's own terms matter, or a contract with X.
- It's your own account. Owned Reads cost $0.001 an item with your own app.
- Only public posts? Then you may skip OAuth: Weir takes one key, with 5,000 free credits a month.
FAQ
What is the difference between OAuth 1.0a and OAuth 2.0 in the X API?
OAuth 1.0a signs each request with four keys, and its user tokens don't expire. OAuth 2.0 uses PKCE, named scopes and two-hour tokens, and works with the v2 API only.
Is the X API bearer token OAuth 2.0?
Yes, the app-only kind. Copy it from the console, or post your API key and secret to /oauth2/token with grant_type=client_credentials. It reads public data and can't act as a user.
Do I need a client secret for X OAuth 2.0 PKCE?
Only for confidential clients: Web App, and Automated App or bot. Native and single-page apps send client_id in the body and rely on the verifier.
How long does a Twitter API OAuth 2.0 token last?
Two hours. With offline.access you also get a refresh token, which lasts about six months and works once.
Sources
All checked on 24 October 2026, docs.x.com read as Markdown:
- X auth overview, bearer tokens and OAuth 1.0a: https://docs.x.com/fundamentals/authentication/overview, https://docs.x.com/fundamentals/authentication/oauth-2-0/bearer-tokens and https://docs.x.com/fundamentals/authentication/oauth-1-0a/obtaining-user-access-tokens
- X OAuth 2.0 with PKCE: https://docs.x.com/fundamentals/authentication/oauth-2-0/authorization-code and https://docs.x.com/fundamentals/authentication/oauth-2-0/user-access-token
- X token exchange (21 September 2026): https://docs.x.com/fundamentals/authentication/oauth-2-0/oauth-1-0a-token-exchange
- X apps and callbacks: https://docs.x.com/fundamentals/developer-apps
- X XDK, xurl and MCP docs: https://docs.x.com/xdks/python/authentication, https://docs.x.com/tools/xurl and https://docs.x.com/tools/mcp
- xurl source, v1.3.4: https://github.com/xdevplatform/xurl
- X OpenAPI 2.172 and pricing: https://api.x.com/2/openapi.json and https://docs.x.com/x-api/getting-started/pricing
- RFC 7636, Appendix B: https://datatracker.ietf.org/doc/html/rfc7636
- Weir: https://docs.weirapp.io/authentication and https://docs.weirapp.io/mcp
- Our runs against X's token endpoint, with fake values