X (Twitter) API · Access & Permissions

The developer console says your app has “Read and write”. Your Direct Message call still fails. That is not a bug, and regenerating your keys will not fix it. X runs two separate permission systems side by side, and the one your token was minted under — not the label on your app — decides what that token can actually do.

The part that breaks apps already in production

Raising an app’s permission level in the console does not upgrade tokens that were already issued. X states it plainly: changing permissions requires users to re-authorise your app to get new tokens with the updated scope. Every token you already hold keeps its old access level until that specific user walks through the authorisation flow again. Teams usually discover this after the switch is live, with a user base split across two permission states and no clean way to tell them apart from the token alone.

Almost every “X API returns 403” thread is really a permissions-model question wearing an error code. Before you can fix it you have to know which of X’s two authorisation systems you are in, what that system actually grants, and which endpoints are simply unreachable from it. This page maps that out — and flags the places where X’s own live documentation disagrees with itself.

Two permission systems, not one

An X app is not a single permission object. Depending on how it was configured at creation, it can carry both of these at the same time, each minting its own credentials with its own rules.

OAuth 1.0a permission level

One coarse setting for the whole app: read only, read and write, or read, write and DMs. It applies to every user who authorises the app.

OAuth 2.0 scopes

Fine-grained named capabilities requested per authorisation. Two users of the same app can hold tokens with completely different scope sets.

App-only Bearer token

No user context at all. Public data endpoints only — and some endpoints are reachable only this way.

Both enabled at once

An app can have OAuth 1.0a and OAuth 2.0 turned on together. The credentials are separate, the limits differ, and mixing them is where most confusion starts.

X’s current guidance is to choose OAuth 2.0 for new projects, because it offers fine-grained scopes and is required for X API v2 user-context endpoints. That advice is easy to read and expensive to implement badly.

What “Read and write” actually excludes

The OAuth 1.0a model has exactly three levels, and the boundary between them is not where most people assume.

Permission levelGrantsDoes not grant
Read onlyView posts, users and public dataCannot post, like, or modify anything. Cannot access Direct Messages.
Read and writeAll read permissions, plus posting and deleting posts, following and unfollowing, liking and repostingCannot access Direct Messages.
Read, write and DMsAll read and write permissions, plus sending and reading Direct MessagesRequires every existing user token to be re-issued after the change

The definition of a “write” is broader than it sounds. X’s OAuth FAQ puts it this way: any API method that requires an HTTP POST is considered a write method and requires read and write access. Read-only apps therefore fail on a long list of calls that do not feel like writes to the person building them.

Scopes are named capabilities, and they do not nest

OAuth 2.0 replaces the three-level ladder with a list of individually requested scopes. There is no hierarchy — holding tweet.write grants you nothing about likes, lists, bookmarks or DMs. Each capability has to be asked for explicitly at authorisation time, and anything you forget cannot be added to an existing token.

Capability areaScopes X documentsWorth knowing
Poststweet.read, tweet.write, tweet.moderate.writeHiding and unhiding replies is its own scope, separate from posting
Usersusers.read, users.emailEmail address is a distinct scope, not part of profile read
Social graphfollows.read, follows.write, block.read, block.write, mute.read, mute.writeRead and write are always separate scopes, never bundled
Engagementlike.read, like.write, bookmark.read, bookmark.writeBookmarks are documented for OAuth 2.0 user context only
Lists & Spaceslist.read, list.write, space.readCreating a List is documented as needing both list scopes
Direct Messagesdm.read, dm.writeThe only route to DM data under OAuth 2.0 — nothing else implies it
Media & sessionmedia.write, offline.accessoffline.access is what makes your integration survive past two hours

Read X’s endpoint-to-authentication mapping before you finalise a scope list, because the asymmetries are real. Nearly every user-context row requires the base pair tweet.read + users.read in addition to the capability scope. Bookmark endpoints have no OAuth 1.0a column at all. Full-archive search, filtered stream and volume streams run the other way — App-only Bearer token, with no user-context option listed.

The token-lifetime trap

The two systems age completely differently, and this catches teams migrating from 1.0a. An OAuth 2.0 access token from the authorisation code flow with PKCE stays valid for two hours unless you requested offline.access; without that scope X does not generate a refresh token at all, and you cannot retro-fit one — the user has to re-authorise. The authorisation code itself has a 30-second exchange window. OAuth 1.0a access tokens, by contrast, are not explicitly expired; they die only when a user revokes the app or X suspends it. An app that assumed the 1.0a behaviour and shipped without offline.access will look like it is randomly logging users out.

The official docs contradict themselves — and it ships as a bug

On the same OAuth 2.0 page, X’s scope table names the follow scopes follows.read and follows.write, while the worked authorize-URL example a few paragraphs below uses account.follows.read and account.follows.write. Both are live, official, and on one page. A team that copies the example rather than the table builds an authorisation request against scope names that do not appear in the reference list.

It is not the only drift. The scope descriptions still say “Tweets” throughout while the product and the endpoints say posts. The PKCE parameters still document plain alongside S256 as an accepted code_challenge_method. And the app-type choice made at creation — Web App and Automated App or Bot are confidential clients with a client secret, Native App and Single Page App are public clients using PKCE only — is easy to get wrong in a console form and awkward to unwind later.

Where these integrations actually stall

  • Scope list written by whoever built the login button, not derived from the endpoint map — so it is short by one scope and fails on a single feature.
  • App type chosen wrong at creation. A server-side product registered as a Native App or SPA is a public client with no client secret.
  • No offline.access. Everything works in testing and users get silently disconnected two hours later in production.
  • Permission level raised after launch with no re-consent plan, leaving a token estate split across two access levels.
  • Callback URL problems. Exact-match validation, a ten-URL cap per app, http://127.0.0.1 rather than localhost for local work, and a long list of disallowed protocols.
  • Assuming a documented scope is reachable from the access path the app is actually on, rather than checking the endpoint mapping.
  • Coding to the example instead of the reference when the two disagree — see the follow-scope naming above.

What a properly scoped app looks like

The work is not typing a scope string. It is deciding the permission architecture before any user consents to anything, because every mistake at this layer costs a re-consent cycle with real users.

  1. Endpoint inventory firstEvery call the product will make, now and in the next two quarters, listed before a single credential is generated.
  2. Auth method decided per endpoint, not per appSome endpoints are user-context only, some are App-only Bearer only. That split, not preference, drives the design.
  3. Minimum viable scope setThe smallest set that covers the inventory — X’s own best-practice guidance is to request only the permissions the app actually needs.
  4. Consent and re-consent strategyHow existing users are moved when the scope set changes, and how the app detects which state a given token is in.
  5. Token lifecycle and failure handlingRefresh behaviour, revocation, suspension, and what the product does when a token comes back invalid mid-session.

What getting it right looks like

One consent screenUsers authorise once, with a scope set that covers the whole product
No silent 403sPermission failures caught at design time, not by customers
Tokens that surviveRefresh handled deliberately instead of discovered in an incident

Getting this right before it reaches users

X’s permission model is not difficult because any single rule is hard. It is difficult because the rules live across four documentation pages that do not fully agree, the consequences of a wrong choice land on your users rather than on you, and the fix is almost always a re-authorisation campaign you would rather not run.

I work on API access, permission and approval problems across Meta, Google, LinkedIn and X — including permission-model design, scope and consent architecture, and untangling integrations that are already live and already failing. The same class of problem shows up on other platforms too: see Facebook Login permissions and when app review kicks in and the Google OAuth sensitive and restricted scope review. A full list of what I take on is on the services page.

Everything above is drawn from X’s official developer documentation as published at the time of writing. Platform requirements change without notice, and no specific outcome or timeline can be guaranteed by anyone, including me. Verify against the current official documentation before you build.