Post on X as someone else

OAuth 2.0 + PKCE — no username/password sharing. Official X API user-context tokens.

POST /2/tweets · createPosts OAuth 2.0 PKCE Python 3.10+ stdlib docs checked 2026-07-12
Open implementation on GitHub →
Never share passwords. X does not use password login for API posting. The account owner authorizes your developer app on an official X page. You store tokens; they can revoke anytime at Connected apps.

Person A gives access to Person B

RoleWhoWhat they do
Person A Account owner (timeline to post on) Opens the authorize link while logged into their X account → clicks Allow
Person B Operator / developer (will post) Owns the developer app, runs authorize, keeps tokens, runs post
That “Allow” click is the entire grant. A never shares a password. B never logs in as A. X issues tokens to B’s app for A’s user. A revokes anytime under Settings → Connected apps.
  1. B creates the X developer app + sets callback URI.
  2. B runs python -m x_posting.authorize (callback server up).
  3. B sends the printed authorize URL to A.
  4. A opens it on X → Allow.
  5. Browser returns to B’s callback; tokens saved on B’s machine.
  6. B posts: python -m x_posting.post "…"
B runs local callback server │ ▼ B sends authorize URL → A opens it (logged into A’s X) https://x.com/i/oauth2/authorize │ ▼ A clicks Allow │ ▼ X redirects → B’s callback ?code=&state= │ ▼ B: POST /2/oauth2/token → access_token + refresh_token │ ▼ B: POST /2/tweets (posts appear as A) Authorization: Bearer <user_access_token>
Remote A + laptop B: default callback is 127.0.0.1 on B’s machine. After Allow, the browser must reach B’s server — use a screen-share on B’s PC, or put an HTTPS tunnel / public callback in the portal and X_REDIRECT_URI.

Official references

TopicDoc
Create or Edit Post docs.x.com/x-api/posts/create-post
OpenAPI operationId createPosts · POST /2/tweets
OAuth 2.0 PKCE authorization-code
User access token steps user-access-token
Pricing getting-started/pricing

API: createPosts

Security for POST /2/tweets requires user context:

POST https://api.x.com/2/tweets
Authorization: Bearer <USER_ACCESS_TOKEN>
Content-Type: application/json

{"text":"Hello from delegated OAuth"}

Success: HTTP 201 with data.id and data.text.

Do not use the app-only Bearer token from the developer portal. That cannot post as a user.

OAuth endpoints

StepEndpoint
Authorize dialogGET https://x.com/i/oauth2/authorize
Exchange / refreshPOST https://api.x.com/2/oauth2/token
RevokePOST https://api.x.com/2/oauth2/revoke

Access tokens last about 2 hours. Request scope offline.access to get a refresh_token. Authorization codes expire in about 30 seconds — exchange immediately.

Developer Console checklist

  1. Create an App in the X Developer Console.
  2. Enable OAuth 2.0 User authentication. App type: Web / Automated / Bot (confidential client).
  3. App permissions: Read and write.
  4. Callback URI: http://127.0.0.1:8787/callback (exact match for this repo’s default).
  5. Copy Client ID and Client Secret.
  6. Ensure your plan can call create post (pay-per-use credits if applicable).

Run the ready implementation

git clone https://github.com/rainbowpuffpuff/x-posting-as-other.git
cd x-posting-as-other
cp .env.example .env
# put X_CLIENT_ID and X_CLIENT_SECRET in .env

export PYTHONPATH=src
python -m x_posting.authorize
# → account owner opens the printed URL and clicks Allow

python -m x_posting.post "Hello from OAuth — no password shared"
python -m x_posting.refresh
python -m x_posting.revoke

Code map (precise functions)

FunctionRole
pkce.generate_code_verifier / code_challenge_s256PKCE S256
oauth.build_authorize_urlBuild x.com/i/oauth2/authorize URL
server.OAuthCallbackServerLocal /callback HTTP server
oauth.exchange_codePOST /2/oauth2/token authorization_code
oauth.refresh_access_tokenPOST /2/oauth2/token refresh_token
oauth.revoke_tokenPOST /2/oauth2/revoke
posts.create_postPOST /2/tweets · createPosts
posts.get_meGET /2/users/me

Scopes this repo requests

tweet.read tweet.write users.read offline.access
ScopeWhy
tweet.writeCreate posts
tweet.readListed in createPosts OAuth2 security
users.readIdentify who authorized
offline.accessRefresh token for long-lived access

What Person B tells Person A

“I’m not asking for your password. Open this official X link while logged into your account and click Allow so my app can post for you. You can revoke anytime under Settings → Connected apps. I only store API tokens.”

Common failures

SymptomLikely cause
Redirect errorredirect_uri not exact match in portal
403 on postApp-only Bearer, missing write, or plan/credits
401 after ~2hNo refresh / missing offline.access
Token exchange failsCode expired (>30s) or already used