X API reference

OAuth 2.0 with PKCE, as X implements it

PKCE is the authorization code flow hardened for clients that cannot keep a secret: the user approves a named list of scopes on x.com, your app redeems a one-time code together with a proof it generated beforehand, and no password ever passes through your software.

Last verified Primary source: docs.x.com — bookmarks by user (auth and scopes)

What PKCE adds to the plain flow

In the classic authorization code flow, the app proves it is itself by sending a client secret when it redeems the code. That works for a server and fails for anything a user can inspect — a single-page app, a mobile binary — because a secret shipped to a device is not a secret. PKCE replaces the stored secret with a value the client invents for each attempt.

  1. Generate a verifier and derive a challenge

    The client makes a high-entropy random string, the code_verifier, and sends only its SHA-256 hash — the code_challenge, with code_challenge_method=S256 — to the authorize endpoint. The verifier never leaves the client at this stage.
  2. Send the user to x.com to approve

    With the challenge, your client ID, redirect URI, the scopes you want, and a state value you generated. X shows the consent screen listing those scopes in the user’s own words.
  3. Receive the code on your redirect URI

    Check state matches what you issued before doing anything else. This is what stops a third party from walking a victim through a login that binds their session to someone else’s account.
  4. Redeem the code with the verifier

    Now the client sends the original code_verifier. X hashes it and compares it against the challenge from step one. An attacker who intercepted the code has no verifier and cannot use it, which is the entire point of the exercise.
  5. Store the tokens encrypted

    You get an access token and, if you asked for offline.access, a refresh token. Both are bearer credentials for someone else’s account and belong encrypted at rest, not in a plain column.

The scopes worth asking for, and no more

ScopeWhat it grantsWhy request it
tweet.readReading post contentWithout it the bookmark list comes back with no posts in it.
users.readReading user objectsNeeded to resolve author handles and the signed-in user's own ID.
bookmark.readReading the account's bookmarksThe one that makes the endpoint work at all.
offline.accessA refresh tokenWithout it, access ends when the token expires and the user has to reconnect by hand.
Four scopes, all read-only. There is no write scope in this list, which means there is no write capability to misuse.

Scope minimisation is usually argued on principle. Here it is also the commercial argument: the consent screen is the last thing between a visitor and your product, and a screen that says “read your bookmarks” converts better than one that also asks to post, follow and send messages. A read-only app cannot damage an account even if it is compromised, and being able to say so plainly is worth more than any feature a write scope would enable.

Refresh rotation: the failure that looks like nothing

X rotates refresh tokens on use. Redeeming a refresh token returns a new access token and a new refresh token, and the one you just used is spent. If your code stores the new access token and forgets to overwrite the refresh token, everything works perfectly until the access token expires — and then that user is permanently disconnected, because the only refresh token you hold has already been consumed.

the write that people forget
const next = await refresh(stored.refreshToken);

await saveTokens(userId, {
  accessToken:  next.access_token,
  refreshToken: next.refresh_token,   // ← rotated. Persist it or lose the user.
  expiresAt:    Date.now() + next.expires_in * 1000,
});

This fails per user and silently. Nothing throws, no dashboard turns red, and the symptom arrives days later as one person quietly receiving no more updates. Write the persistence of the rotated token in the same statement as the access token, and treat a refresh failure as “ask this user to reconnect” rather than as a retryable error.

What people get wrong

Reaching for OAuth 1.0a. Plenty of surviving tutorials use it, and the request-signing dance is memorable enough that people look for it. The v2 endpoints described in these pages want OAuth 2.0 user context.

Skipping state because PKCE is already there. They defend different things. PKCE stops a stolen code being redeemed; state stops a forged callback being accepted. You want both.

Assuming permission is the expensive part. Authorisation is free and unlimited. What costs money is what you do with the token afterwards — every post the API hands back is a billable read, and the schedule you read on is governed by the daily deduplication window. Consent is not the constraint; consumption is.

We built the thing this describes

Sift reads your bookmarks once, sorts them into categories, and emails you what you saved. The rules on this page are the reason it costs what it costs — a one-result probe instead of a full page, one paid pass per UTC day, and never an expansion parameter.

Connect X