Twitter Write Sessions | Post as Your X Account
Register your own X session once, or pass credentials per request. Covers proxy_url egress routing and exactly what is stored.
Read endpoints pull public data and need nothing but your API key. Write endpoints are different. A like, a retweet, a bookmark, or a follow has to belong to a real X account, and TwitterAPIs runs every write as your account, not a shared pool. So before you can write, you register your X session once. After that, the write endpoints act as you until that session expires.
Why writes need a session
A read call asks X a public question, so any account can ask it. A write call changes state on a specific account: it adds a like to your timeline, a follow to your graph, a bookmark to your list. There is no way to do that anonymously, and we will not borrow someone else's account to do it for you. The write has to run as the account it affects.
That account is yours. You hand us your X session once, we attach it to your API key, and from then on a write call posts as your account. Reads are unaffected and keep working with just the bearer token.
Writes are billed per call
Most successful writes cost $0.0008, the same as a standard read; posting a new tweet (tweet/create) is $0.0016. A write that fails before it reaches X (for example a session_required rejection) is not billed.
What a session is
Your X session is two cookies from a logged-in x.com browser tab: auth_token and ct0. Together they identify your account to X the same way your browser does. You read them once and register them with TwitterAPIs, and we use them to sign your write calls.
The supported documented way to obtain a session is to bring your own cookies.
- Bring your own cookies. Copy
auth_tokenandct0from a logged-inx.comtab and register them viaPOST /twitter/customer/session. This is reliable, instant, and works on every account type.
For anything you want to run on a schedule, register cookies.
Reading your cookies
In a logged-in x.com tab, open your browser dev tools, go to the Application (or Storage) panel, and find Cookies for https://x.com. Copy the values of auth_token and ct0. Treat both like passwords: anyone holding them can act as your account.
Register your session
Send the two cookies as a JSON body to POST /twitter/customer/session, authenticated with your API key. You register once; the session stays attached to your key until it expires.
const res = await fetch("https://api.twitterapis.com/twitter/customer/session", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.TWITTERAPIS_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
auth_token: process.env.X_AUTH_TOKEN,
ct0: process.env.X_CT0,
}),
});
if (!res.ok) {
const err = await res.json();
throw new Error(`${err.error}: ${err.message}`);
}
console.log("Session registered. Write endpoints now act as your account.");import os
import requests
res = requests.post(
"https://api.twitterapis.com/twitter/customer/session",
headers={"Authorization": f"Bearer {os.environ['TWITTERAPIS_KEY']}"},
json={
"auth_token": os.environ["X_AUTH_TOKEN"],
"ct0": os.environ["X_CT0"],
},
)
res.raise_for_status()
print("Session registered. Write endpoints now act as your account.")Register from your backend only
The cookies grant full control of your X account. Send them from a server you control, store them in environment variables or a secret manager, and never put them in browser code or a committed file.
The body takes two more optional fields.
| Field | Required | Description |
|---|---|---|
auth_token | Yes | The auth_token cookie value. |
ct0 | Yes | The ct0 CSRF cookie value. |
user_agent | No | User-agent string sent with every call on this session. Defaults to a current Chrome UA. |
proxy_url | No | HTTP or SOCKS proxy the session's traffic egresses through. Defaults to direct. |
Routing through your own proxy
X associates an account with the IP address it acts from, so an account that normally logs in from a home connection and then starts writing from somewhere else is easier to flag. proxy_url is how you keep your account's traffic on an egress you control.
It is a property of the session, not of a single call. Register it once alongside your cookies and every request made with that API key on that session goes through it, reads as well as writes.
{
"auth_token": "abc123...",
"ct0": "def456...",
"proxy_url": "http://user:pass@proxy.example.com:8000"
}The value is a full proxy URL. HTTP, HTTPS, and SOCKS5 schemes are accepted, and credentials go inline in the userinfo part as shown. Anything empty, absent, or the literal string direct means no proxy.
Overriding it for one request
Write endpoints also accept a proxy on the request itself, which takes precedence over the registered one for that call only. Three spellings work, and they are read in this order:
- the
?proxy=query parameter - the
x-proxy-urlheader - a
proxyorproxy_urlfield in the JSON body
Read endpoints do not take a standalone per-call proxy. They use the session's own proxy_url, or the x-proxy-url you send alongside per-request credentials (below).
If you register no proxy
direct does not promise a direct connection from a fixed address. It means you have not told us where to egress from, so we choose, and the address a write leaves from is then ours rather than yours. If your account needs a stable, known egress IP, or your own security review needs to name it, register a proxy_url.
Credentials per request, without registering
Registering is convenient but it means we hold your cookies. If you would rather we never did, send them on the request instead. They are used for that single call and are never written to storage.
Headers are the recommended form, because they work the same on GET and POST:
| Header | Required | Description |
|---|---|---|
x-auth-token | Yes | The auth_token cookie value. |
x-ct0 | Yes | The ct0 CSRF cookie value. |
x-user-agent | No | User-agent for this call. |
x-proxy-url | No | Proxy for this call. |
On a POST you can send the same four as JSON body fields instead: auth_token, ct0, user_agent, proxy_url. Headers win if both are present. Both cookies must be there; one without the other is ignored and the call falls back to your registered session.
curl "https://api.twitterapis.com/twitter/dm/list" \
-H "Authorization: Bearer $TWITTERAPIS_KEY" \
-H "x-auth-token: $X_AUTH_TOKEN" \
-H "x-ct0: $X_CT0"Because the credentials ride on the request, one API key can act as many different accounts. That is the model for polling several accounts' inboxes, or posting from a set of accounts, without registering a session per key.
Where per-request credentials are accepted
This is the complete list
Fifteen endpoints read per-request credentials. Everywhere else the headers are ignored and the call runs on the key's registered session, or returns 409 session_required if there is none. That failure is quiet, so check this list before you build on it.
| Endpoint | |
|---|---|
POST /twitter/tweet/create | Post a tweet |
POST /twitter/tweet/delete | Delete a tweet |
GET /twitter/user/bookmarks | Read bookmarks |
GET /twitter/user/bookmark_search | Search bookmarks |
GET /twitter/user/blocking | Read the block list |
GET /twitter/user/muting | Read the mute list |
GET /twitter/dm/list | Read DM threads |
GET /twitter/dm/conversation | Read one DM thread |
POST /twitter/article/create | Start a draft article |
POST /twitter/article/update_title | Set an article's title |
POST /twitter/article/update_content | Set an article's body |
POST /twitter/article/publish | Publish an article |
POST /twitter/article/unpublish | Revert an article to draft |
GET /twitter/article/list | List your own articles |
POST /twitter/article/delete | Delete an article |
The engagement writes (tweet/favorite, tweet/retweet, tweet/bookmark, user/follow and their inverses), dm/send, user/home_timeline, user/likes, media/upload, and user/update_profile all run on the registered session only. article/get is different again: it is a PUBLIC read that needs no session and no per-request credentials at all, just the API key.
When per-request credentials are rejected by X, the session_dead response tells you to send fresh ones rather than to re-register, since there is nothing registered to replace.
Run a write
Once the session is registered, the write endpoints take your API key as usual and act as your account. Here is a favorite (like) on a tweet by its ID.
const res = await fetch("https://api.twitterapis.com/twitter/tweet/favorite", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.TWITTERAPIS_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ tweet_id: "1759123456789012345" }),
});
const data = await res.json();
console.log(data);import os
import requests
res = requests.post(
"https://api.twitterapis.com/twitter/tweet/favorite",
headers={"Authorization": f"Bearer {os.environ['TWITTERAPIS_KEY']}"},
json={"tweet_id": "1759123456789012345"},
)
print(res.json())The same pattern covers every write action. Each takes your API key, runs as your registered account, and returns the standard {error, message} envelope on failure.
| Endpoint | Action |
|---|---|
POST /twitter/tweet/favorite | Like a tweet |
POST /twitter/tweet/unfavorite | Remove a like |
POST /twitter/tweet/retweet | Retweet |
POST /twitter/tweet/unretweet | Undo a retweet |
POST /twitter/tweet/bookmark | Bookmark a tweet |
POST /twitter/tweet/unbookmark | Remove a bookmark |
POST /twitter/user/follow | Follow a user |
POST /twitter/user/unfollow | Unfollow a user |
If you skip registration
A write call before you have registered a session returns 409 with session_required. It means the request was valid but there is no account attached to your key yet. Register via POST /twitter/customer/session, then retry the same call.
{
"error": "session_required",
"message": "No X session is registered for this API key. Register one via POST /twitter/customer/session before writing."
}This response is not billed. Branch on the error field, register the session, and retry.
When a session goes dead
X sessions do not last forever. Logging out of that x.com tab, a password change, or X expiring the cookies will invalidate the session you registered. When that happens, a write returns 401 with a session-dead error: the key is fine, but the X account behind it can no longer act.
{
"error": "session_dead",
"message": "The registered X session is no longer valid. Re-register fresh cookies via POST /twitter/customer/session."
}Recover by reading fresh auth_token and ct0 cookies from a logged-in tab and registering them again at POST /twitter/customer/session. The new session replaces the old one and writes resume.
Build for re-registration
Treat session death as a normal event, not an outage. Catch session_dead on writes, alert whoever owns the account, and re-register. A scheduled job that does writes should expect to refresh its session periodically.
What we hold, and for how long
The two paths above have different storage postures, and the difference is the reason to pick one over the other.
Per-request credentials are not stored. Cookies sent as x-auth-token / x-ct0 headers, or in the JSON body, build a session that exists for the duration of that one call and is then dropped. Nothing is written to the sessions table. Our own test suite asserts that the table is still empty after a call made this way.
Registered credentials are stored, encrypted. auth_token and ct0 are held against your API key and encrypted at rest with AES-256-GCM. There is a single write path, so nothing bypasses the encryption, and the read path refuses a value it cannot decrypt rather than falling back to using it as-is. Encryption at rest defends a copied database file, a backup, or a snapshot. It does not defend a compromised server, where the key and the data are reachable together.
A registered pair stays until you replace it. Re-registering on the same API key overwrites both cookies. When X invalidates a session we mark it dead and stop using it, though the row itself remains.
Removal is on request today
There is currently no self-serve endpoint to delete a stored session and no automatic expiry after a period of inactivity. If you want a registered session removed, email emma@twitterapis.com and we will delete it. If your policy requires that no third party ever holds the credential, use the per-request path instead of registering.
Where to go next
Authentication
How the bearer token works on every request, read or write.
Errors
Status codes, the JSON error shape, and how to handle each.
Rate limits
How backoff works and what a 429 means.
FAQ
Why do writes need a session?
A write changes state on a specific account: a like, a follow, a bookmark. It must run as that account, so you register your own X session first.
What is an X session here?
Two cookies from a logged-in x.com tab: auth_token and ct0. Register them via POST /twitter/customer/session. Treat both like passwords.
What happens when a session expires?
A write returns 401 with session_dead. Read fresh auth_token and ct0 cookies and re-register them. The new session replaces the old one.
How does proxy_url work?
proxy_url is an optional field on POST /twitter/customer/session. It is a property of the session, so once registered every call on that API key egresses through it, reads and writes alike. Write endpoints also accept a per-call override via ?proxy=, the x-proxy-url header, or a proxy or proxy_url body field, which wins for that request only. Omitting it stores the session as direct, which means we choose the egress.
Can I use write endpoints without you storing my cookies?
Yes. Send auth_token and ct0 as x-auth-token and x-ct0 headers, or in the JSON body on a POST, and they are used for that single call and never written to storage. Fifteen endpoints accept them: tweet/create, tweet/delete, user/bookmarks, user/bookmark_search, user/blocking, user/muting, dm/list, dm/conversation, article/create, article/update_title, article/update_content, article/publish, article/unpublish, article/list, and article/delete. Everywhere else the call runs on the registered session.
How are registered cookies stored, and can they be deleted?
auth_token and ct0 are stored against your API key and encrypted at rest with AES-256-GCM, and the read path refuses a value it cannot decrypt rather than using it as-is. They persist until you replace them by re-registering. There is no self-serve delete endpoint and no automatic expiry today, so email emma@twitterapis.com to have a stored session removed.