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.
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.
Generate a verifier and derive a challenge
The client makes a high-entropy random string, thecode_verifier, and sends only its SHA-256 hash — thecode_challenge, withcode_challenge_method=S256— to the authorize endpoint. The verifier never leaves the client at this stage.Send the user to x.com to approve
With the challenge, your client ID, redirect URI, the scopes you want, and astatevalue you generated. X shows the consent screen listing those scopes in the user’s own words.Receive the code on your redirect URI
Checkstatematches 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.Redeem the code with the verifier
Now the client sends the originalcode_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.Store the tokens encrypted
You get an access token and, if you asked foroffline.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
| Scope | What it grants | Why request it |
|---|---|---|
tweet.read | Reading post content | Without it the bookmark list comes back with no posts in it. |
users.read | Reading user objects | Needed to resolve author handles and the signed-in user's own ID. |
bookmark.read | Reading the account's bookmarks | The one that makes the endpoint work at all. |
offline.access | A refresh token | Without it, access ends when the token expires and the user has to reconnect by hand. |
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.
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