WEIR

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.


Published · Updated · 6 min read

Cover image: PKCE, from verifier to refresh

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
  1. Turn on OAuth 2.0Pick an app type and add your callback URL.
  2. Build the authorize URLHash a verifier into an S256 challenge.
  3. Swap the codePOST it to the token endpoint, fast.
  4. 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 the x.com one.

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.

30 secondsto swap the code before it expires
2 hoursfor each access token
6 monthsabout, for each single-use refresh token

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.read and users.read: nearly every user call, even GET /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 .write twin.
  • 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_uri differs 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_token came back: add offline.access and 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: