X-Autopilot

X API OAuth: pick the right auth for each endpoint

App-only, OAuth 1.0a or OAuth 2.0 with PKCE? The endpoints each one can reach, the scope table, token lifetimes, and the 2026 consent-screen change.

X-Autopilot Team··9 min read
On this page · 8 sections

The short version

  • ▸There are three methods and one question that picks between them: does the call need a specific user's permission? No means an app-only bearer token. Yes means OAuth 2.0 Authorization Code Flow with PKCE, or legacy OAuth 1.0a if you still have it wired.
  • ▸The split is not read versus write. Filtered stream, volume stream and full-archive search accept app-only tokens ONLY, so a user token cannot reach them. Bookmarks accept user tokens only. Several endpoint groups accept one method and reject the other two.
  • ▸OAuth 2.0 access tokens die after 2 hours. You only get a refresh token if you asked for the offline.access scope, and refresh tokens are single-use with roughly a 6-month life, so each refresh has to overwrite the stored one.
  • ▸Since 21 September 2026 you can trade a stored OAuth 1.0a token for an OAuth 2.0 pair with a token-exchange grant: no consent screen, no user notification, scopes derived from what the user already allowed. X says OAuth 1.0a is being retired, with no published date.
  • ▸Pay-per-use apps asking for any write scope or dm.read now show a 'Sensitive permissions requested' disclaimer and an 'I trust this app' checkbox on the consent screen. X states there is no way to remove it, so build your conversion expectations around it.

Quick answer

X API OAuth comes in three flavours and one question picks between them: does this call need a specific user's permission? If no, use an app-only OAuth 2.0 bearer token. If yes, use OAuth 2.0 Authorization Code Flow with PKCE and request the narrow scopes you need. OAuth 1.0a user context still works but X is retiring it, and since 21 September 2026 you can convert your stored 1.0a tokens to OAuth 2.0 silently with a token-exchange grant.

Last updated: October 2026

That is the whole decision in one paragraph. The reason X API authentication still eats a day of anyone's build is everything after the decision: several endpoint groups accept exactly one of the three methods, tokens expire on four different clocks, and the consent screen your users now see changed last month.

The three methods, and what each one actually is

MethodRepresentsExpiresUse it for
App-only bearer tokenYour appOn revocationPublic reads, streams, archive search
OAuth 2.0 Authorization Code with PKCEA consenting user2 hoursAnything on a user's behalf
OAuth 1.0a user contextA consenting userOn revocationLegacy integrations, standalone Ads API

The app-only token is the one you copy out of the Keys and Tokens page after you get an X API key. It is a static string, it carries app-level rate limits, and it cannot act as a person. You can also mint it yourself by passing your API key and secret to POST oauth2/token.

OAuth 2.0 with PKCE is the modern user path. You build an authorize URL pointing at x.com/i/oauth2/authorize, the user approves the scopes you listed, X redirects back with a code, and you trade that code for an access token. Picking fine-grained scopes is the point: the consent screen only shows what you asked for, so a reader app that requests tweet.read users.read presents a much easier decision than one asking for write access.

OAuth 1.0a is the signed-request method from the Twitter era. It still authenticates most v2 endpoints, and X's own token-exchange documentation now says plainly that it is being retired, so treat it as debt rather than a choice.

One detail that trips up server-side builds: your app type decides whether you get a client secret at all. X classifies Native App and Single Page App as public clients, and Web App and "Automated App or bot" as confidential clients. Only confidential clients receive a client secret and authenticate the token endpoint with an Authorization: Basic header; public clients must send client_id in the request body instead.

Which method each endpoint group accepts

This is the part the tutorials skip, and it is the part that costs you an afternoon. The split is not read versus write.

Endpoint groupApp-onlyOAuth 2.0 userOAuth 1.0a
Post and user lookup, recent search, timelinesyesyesyes
Filtered stream, volume streamyesnono
Full-archive searchyesnono
Create or delete a post, repost, like, follow, mutenoyesyes
Hide a replynoyesyes
Bookmarks lookup and managementnoyesno
Spaces lookup and searchyesyesno
Space ticket buyersnoyesno

Read that second row twice. The filtered stream and the 1% volume stream accept an app-only bearer token and refuse user tokens, so the natural instinct of "I have a user token, I will use it everywhere" fails on the exact endpoints a monitoring tool is built on. Most real integrations end up holding both kinds of credential at once: an app token for the streams and public reads, a per-user token for anything that writes.

Bookmarks run the other way. There is no app-only path, because a bookmark list is private by definition, so bookmark features need bookmark.read or bookmark.write from the account that owns them.

If you need a user's activity pushed to you rather than polled, that is a different system with its own permission model: the X Activity API splits events into public ones an app token can subscribe to and private ones that need the user's OAuth consent plus a matching scope.

The scope list

Scopes are granular and additive. The practical set, as published in X's OAuth 2.0 reference:

  • Reads: tweet.read, users.read, users.email, follows.read, like.read, list.read, mute.read, block.read, bookmark.read, dm.read, space.read, broadcast.read
  • Writes: tweet.write, tweet.moderate.write, follows.write, like.write, list.write, mute.write, block.write, bookmark.write, dm.write, media.write, broadcast.write
  • Session: offline.access, which is the only way to get a refresh token

Two rules save debugging time. Almost every user-context endpoint wants tweet.read and users.read in addition to the specific scope, even when it does nothing with posts. And offline.access is not optional in practice: without it your integration dies two hours after the user walks away, because there is nothing to refresh with.

Token lifetimes: four different clocks

CredentialLifetimeReusable
Authorization code30 secondsNo
OAuth 2.0 access token2 hoursYes, until expiry
OAuth 2.0 refresh tokenAbout 6 monthsNo, single use
App-only bearer, OAuth 1.0a tokenUntil revokedYes

A callback handler that does any slow work before the token exchange fails intermittently in production and never in local testing, because of that 30-second code window. Exchange first, then do your database writes.

The single-use refresh token is the other classic bug. Each refresh returns a new refresh token and invalidates the one you sent, so a worker that keeps the original in memory and refreshes twice logs the user out. Overwrite the stored value inside the same transaction that stores the new access token.

Callback URLs need exact-match validation, which means a trailing slash difference is a rejected request rather than a redirect. And on the upside, OAuth 2.0 raises the per-app ceiling for post lookup and user lookup from 300 to 900 requests per 15 minutes, which is a genuine reason to migrate a read-heavy integration.

Added 21 September 2026, and the most useful auth change of the year if you maintain an older app. You can trade each stored OAuth 1.0a token for an OAuth 2.0 pair without the user doing anything:

curl --request POST 'https://api.x.com/2/oauth2/token' \
  --header 'Content-Type: application/x-www-form-urlencoded' \
  --header 'Authorization: Basic <base64(client_id:client_secret)>' \
  --data-urlencode 'grant_type=urn:ietf:params:oauth:grant-type:token-exchange' \
  --data-urlencode 'subject_token=<oauth1_access_token>' \
  --data-urlencode 'subject_token_type=urn:x:params:oauth:token-type:oauth1_token' \
  --data-urlencode 'oauth_token_secret=<oauth1_token_secret>'

What X documents about the behaviour, and it is worth knowing before you script a million of these:

  • Scopes are derived from the permission level the user originally approved. Read maps to the read scopes, read-and-write adds the write scopes, the DM permission adds dm.read and dm.write, and offline.access is always included. You can request a subset, never more.
  • Scopes the original authorization never covered, such as bookmark.read or space.read, are not granted. Those still need a real authorization flow.
  • No consent screen, no notification, no session change for the user.
  • The OAuth 1.0a token stays valid, so the migration is non-destructive and you can run it at your own pace.
  • Re-running the exchange for the same user is safe and returns a fresh pair, but it invalidates the previous pair. Do not run two migration jobs over the same users at once.
  • 400 invalid_grant means that user revoked you or the token died. Do not retry; treat them as churn to re-acquire.
  • The exchange is limited to 10,000 requests per 15 minutes per app per source IP, which is about a million users a day from one worker.

One exception X calls out: the standalone Ads API at ads-api.x.com continues to use OAuth 1.0a, so keep that integration wired. Exchanged tokens do carry ads.read and ads.write for the v2 ads endpoints, and the X Conversion API path works from those.

If your authorization rate dropped in September, this is probably why. On 14 September 2026 X added a disclaimer to the consent screen for pay-per-use apps requesting any write scope or dm.read. The user now sees a "Sensitive permissions requested" notice, a statement that your app is not affiliated with X, a reminder to confirm they trust the callback URL, and a mandatory "I trust this app" checkbox before the Authorize button does anything.

X's developer forum post is blunt about it: there is no way to remove the warning from a pay-per-use app, it is expected behaviour rather than a misconfiguration, and it does not affect token issuance or billing. The practical responses are to request the narrowest scope set that works, delay the write-scope request until the user has a reason to trust you, and put your real brand on the app so the callback URL matches what you promised. A domain that looks nothing like your product name is now a visible risk signal rather than a detail nobody reads.

Worth noting for the other side of this: users can review and remove any of these grants at any time, which is what X connected apps covers. A token your integration relies on can vanish between two requests.

Auth is not the hard part of automation

Getting the handshake right takes a day. What the handshake cannot do is make engagement automation cheap or sanctioned.

Scopes decide what you can call, not what X allows you to do with it. Automated posting, following and replying sit under X's automation rules, which require accountability for what your app does and the automated account label on accounts that post without a human. The API route is the sanctioned one, and it is metered: see X API pricing for what each read, write and webhook event costs now that pay-per-use replaced the old tiers, and X Bots API for the bot-account path that skips a login entirely.

That arithmetic is why plenty of growth work never goes near a token. Replying in your own voice, from your own session, on your own machine is a different trade: no API bill, no scope negotiation, and a smaller detection surface than a cloud tool calling in from a shared IP pool, though a browser-driven workflow is a gray area under those same rules rather than something anyone can call compliant. That is the lane X-Autopilot sits in.

Pick the token for the endpoint, ask for the fewest scopes that work, store the refresh token like it is a password, and the auth layer stops being something you think about.

Frequently asked

Answers indexed by Google + AI assistants.

What authentication does the X API use?+

Three methods, all still live in October 2026. OAuth 2.0 app-only (a bearer token tied to your app, no user involved) for public data. OAuth 2.0 Authorization Code Flow with PKCE for anything done on behalf of a user, with granular scopes. OAuth 1.0a user context, the legacy signed-request method, which X is retiring without a published date. Enterprise endpoints also use HTTP basic auth.

Is a bearer token the same as OAuth 2.0?+

It is one half of it. The bearer token you copy out of the Developer Console is app-only OAuth 2.0: it identifies your app and nothing else, so it can read public posts and users but cannot post, like, follow, or read a DM. The other half, Authorization Code Flow with PKCE, also hands you a bearer token, but that one represents a user who approved specific scopes.

How long does an X API access token last?+

An OAuth 2.0 user access token expires 2 hours after it is issued. The authorization code you exchange for it expires in 30 seconds. A refresh token lasts about 6 months and is single-use, so every refresh returns a new one that must replace the old record. App-only bearer tokens and OAuth 1.0a access tokens do not expire on a timer; they end when revoked.

Can I migrate OAuth 1.0a users to OAuth 2.0 without asking them again?+

Yes, since 21 September 2026. POST the user's stored OAuth 1.0a access token and token secret to /2/oauth2/token with grant_type=urn:ietf:params:oauth:grant-type:token-exchange and you receive an OAuth 2.0 access token plus refresh token for the same user. There is no consent screen and no notification, the granted scopes mirror the original permission level, and the OAuth 1.0a token keeps working afterwards.

Why does my app's consent screen show a scary warning?+

Because it requests a write scope or dm.read on a pay-per-use account. X added a disclaimer on 14 September 2026 that names the sensitive permissions, states the app is not affiliated with X, tells the user to check the callback URL, and requires an 'I trust this app' checkbox. X's developer forum post says this is expected behaviour and that pay-per-use apps cannot remove it.

Which scope do I need to post on behalf of a user?+

tweet.write, plus tweet.read and users.read, which nearly every user-context call needs alongside the specific permission. Hiding a reply needs tweet.moderate.write. Following needs follows.write. Uploading media needs media.write. Reading DMs needs dm.read and sending them needs dm.write, and both now trigger the sensitive-permissions disclaimer.

Related searches
x api oauthx api oauth 2.0 pkcex api bearer token vs oauthtwitter api authentication 2026x api oauth scopes listoauth 1.0a to oauth 2.0 migration x apix api access token expiresx api refresh token offline.accessx api keyx api v2x developer consolex api pay per useoauth consent screenx api rate limitstwitter api python
DY
Deepak YadavBuilding X-Autopilot

Product designer and indie hacker. Runs the agent on his own X account every day and writes up what the data shows, including when it's inconvenient.

Follow on X →
Try X-Autopilot.
$199 once. No subscription, no monthly bill. Real Chrome on your Mac.
See pricing▶
Free PDF · the X Growth Playbook

The exact playbook we use to grow on X

The bio that converts, the daily reply loop, the posting cadence, and the tool stack, in one no-fluff PDF. Drop your email and it's yours.

No spam. Unsubscribe anytime.

Free tools

Try it yourself.

All free X tools →
Keep reading

Related posts.