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
| Role | Who | What 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.
- B creates the X developer app + sets callback URI.
- B runs
python -m x_posting.authorize (callback server up).
- B sends the printed authorize URL to A.
- A opens it on X → Allow.
- Browser returns to B’s callback; tokens saved on B’s machine.
- 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
API: createPosts
Security for POST /2/tweets requires user context:
OAuth2UserToken with scopes tweet.read, tweet.write, users.read
- or OAuth 1.0a user token
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
| Step | Endpoint |
| Authorize dialog | GET https://x.com/i/oauth2/authorize |
| Exchange / refresh | POST https://api.x.com/2/oauth2/token |
| Revoke | POST 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
- Create an App in the X Developer Console.
- Enable OAuth 2.0 User authentication. App type: Web / Automated / Bot (confidential client).
- App permissions: Read and write.
- Callback URI:
http://127.0.0.1:8787/callback (exact match for this repo’s default).
- Copy Client ID and Client Secret.
- 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)
| Function | Role |
pkce.generate_code_verifier / code_challenge_s256 | PKCE S256 |
oauth.build_authorize_url | Build x.com/i/oauth2/authorize URL |
server.OAuthCallbackServer | Local /callback HTTP server |
oauth.exchange_code | POST /2/oauth2/token authorization_code |
oauth.refresh_access_token | POST /2/oauth2/token refresh_token |
oauth.revoke_token | POST /2/oauth2/revoke |
posts.create_post | POST /2/tweets · createPosts |
posts.get_me | GET /2/users/me |
Scopes this repo requests
tweet.read tweet.write users.read offline.access
| Scope | Why |
tweet.write | Create posts |
tweet.read | Listed in createPosts OAuth2 security |
users.read | Identify who authorized |
offline.access | Refresh 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
| Symptom | Likely cause |
| Redirect error | redirect_uri not exact match in portal |
| 403 on post | App-only Bearer, missing write, or plan/credits |
| 401 after ~2h | No refresh / missing offline.access |
| Token exchange fails | Code expired (>30s) or already used |