Post and delete tweets with a session
Register a customer session once with your X credentials, then create and delete tweets programmatically. Full cURL, Python, and Node samples with cost math.
This guide walks through the bring-your-own-credentials write flow: register your X session once, then post tweets (plain text, replies, or quote-tweets) and delete them through the API. The account that acts is the one whose cookies you supply.
Cost per operation: POST /twitter/tweet/create costs $0.0016 per call ($1.60 per 1,000) and POST /twitter/tweet/delete costs $0.0008 per call ($0.80 per 1,000). Session registration (POST /twitter/customer/session) is free; you pay only for the writes you perform.
Write endpoints act as your account
Every tweet you create or delete is performed as the X account whose auth_token and ct0 cookies you registered. The API proxies the action through that session. Never share these credentials outside your own backend.
How it works
- Extract
auth_tokenandct0from your browser cookies on x.com. - Call
POST /twitter/customer/sessiononce to register the session. - Call
POST /twitter/tweet/createto post a tweet. - Call
POST /twitter/tweet/deleteto remove a tweet by ID or URL.
Step 1 - Extract your X cookies
Log in to x.com in your browser. Open DevTools (F12), go to Application > Cookies > https://x.com, and find:
| Cookie name | What it is |
|---|---|
auth_token | Your session token. Treat it like a password. |
ct0 | CSRF token. Must match auth_token. |
Both values are long hex strings. Copy them to environment variables:
export X_AUTH_TOKEN="your_auth_token_here"
export X_CT0="your_ct0_here"
export TWITTERAPIS_KEY="your_api_key_here"Cookie expiry
X session cookies expire after roughly 30-90 days of inactivity. If you get a 401 from a write endpoint, re-extract fresh cookies and re-register the session.
Step 2 - Register the session
Call POST /twitter/customer/session once. You do not need to call it again unless the cookies expire.
Request body:
| Field | Required | Description |
|---|---|---|
auth_token | Yes | The auth_token cookie value. |
ct0 | Yes | The ct0 CSRF cookie value. |
user_agent | No | Browser user-agent to send with session requests. |
proxy_url | No | Optional HTTP or SOCKS proxy to route requests through. |
Response (200):
{
"ok": true,
"message": "Session registered.",
"username": "yourhandle"
}The username field reflects the account X resolved from the cookies. It is best-effort: X may return a blank username if it issues an identity challenge for the session. The session is still registered and functional even when username is empty.
curl -s -X POST https://api.twitterapis.com/twitter/customer/session \
-H "Authorization: Bearer $TWITTERAPIS_KEY" \
-H "Content-Type: application/json" \
-d "{\"auth_token\": \"$X_AUTH_TOKEN\", \"ct0\": \"$X_CT0\"}"import os
import requests
resp = requests.post(
"https://api.twitterapis.com/twitter/customer/session",
headers={
"Authorization": f"Bearer {os.environ['TWITTERAPIS_KEY']}",
"Content-Type": "application/json",
},
json={
"auth_token": os.environ["X_AUTH_TOKEN"],
"ct0": os.environ["X_CT0"],
},
timeout=15,
)
resp.raise_for_status()
data = resp.json()
print(f"Session registered. Username: {data.get('username', '(identity challenge)')}")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) {
throw new Error(`Session registration failed: ${res.status} ${await res.text()}`);
}
const data = await res.json();
console.log(`Session registered. Username: ${data.username ?? "(identity challenge)"}`);Step 3 - Post a tweet
Call POST /twitter/tweet/create. The session you registered in Step 2 is identified by your API key, so no session token is passed here.
Request body:
| Field | Required | Description |
|---|---|---|
text | Yes | Tweet text. Max 280 characters for standard accounts. |
reply_to | No | Tweet ID to reply to. |
quote | No | Tweet ID to quote-tweet. |
Response (200):
{
"ok": true,
"tweet_id": "1890000000000000001",
"url": "https://x.com/yourhandle/status/1890000000000000001"
}Plain text tweet
curl -s -X POST https://api.twitterapis.com/twitter/tweet/create \
-H "Authorization: Bearer $TWITTERAPIS_KEY" \
-H "Content-Type: application/json" \
-d '{"text": "Hello from TwitterAPIs."}'import os
import requests
resp = requests.post(
"https://api.twitterapis.com/twitter/tweet/create",
headers={"Authorization": f"Bearer {os.environ['TWITTERAPIS_KEY']}"},
json={"text": "Hello from TwitterAPIs."},
timeout=15,
)
resp.raise_for_status()
data = resp.json()
print(f"Tweet live: {data['url']}")const res = await fetch("https://api.twitterapis.com/twitter/tweet/create", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.TWITTERAPIS_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ text: "Hello from TwitterAPIs." }),
});
if (!res.ok) throw new Error(`Create failed: ${res.status}`);
const data = await res.json();
console.log("Tweet live:", data.url);Reply to a tweet
Set reply_to to the ID of the tweet you want to reply to:
curl -s -X POST https://api.twitterapis.com/twitter/tweet/create \
-H "Authorization: Bearer $TWITTERAPIS_KEY" \
-H "Content-Type: application/json" \
-d '{"text": "Great point.", "reply_to": "1890000000000000000"}'resp = requests.post(
"https://api.twitterapis.com/twitter/tweet/create",
headers={"Authorization": f"Bearer {os.environ['TWITTERAPIS_KEY']}"},
json={"text": "Great point.", "reply_to": "1890000000000000000"},
timeout=15,
)
resp.raise_for_status()
print(resp.json()["url"])const res = await fetch("https://api.twitterapis.com/twitter/tweet/create", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.TWITTERAPIS_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ text: "Great point.", reply_to: "1890000000000000000" }),
});
const data = await res.json();
console.log(data.url);Quote-tweet
Set quote to the ID of the tweet you want to quote:
curl -s -X POST https://api.twitterapis.com/twitter/tweet/create \
-H "Authorization: Bearer $TWITTERAPIS_KEY" \
-H "Content-Type: application/json" \
-d '{"text": "Worth reading.", "quote": "1890000000000000000"}'Step 4 - Delete a tweet
Call POST /twitter/tweet/delete with either the tweet id or the full status url. You only need one.
Query parameters:
| Parameter | Required | Description |
|---|---|---|
id | One of id / url | Numeric tweet ID, e.g. 1890000000000000001. |
url | One of id / url | Full status URL, e.g. https://x.com/yourhandle/status/1890000000000000001. |
Response (200):
{
"ok": true,
"deleted": true,
"tweet_id": "1890000000000000001"
}# Delete by ID
curl -s -X POST \
"https://api.twitterapis.com/twitter/tweet/delete?id=1890000000000000001" \
-H "Authorization: Bearer $TWITTERAPIS_KEY"
# Delete by URL
curl -s -X POST \
"https://api.twitterapis.com/twitter/tweet/delete?url=https%3A%2F%2Fx.com%2Fyourhandle%2Fstatus%2F1890000000000000001" \
-H "Authorization: Bearer $TWITTERAPIS_KEY"import os
import requests
HEADERS = {"Authorization": f"Bearer {os.environ['TWITTERAPIS_KEY']}"}
def delete_tweet(tweet_id: str) -> bool:
resp = requests.post(
"https://api.twitterapis.com/twitter/tweet/delete",
headers=HEADERS,
params={"id": tweet_id},
timeout=15,
)
resp.raise_for_status()
return resp.json().get("deleted", False)
deleted = delete_tweet("1890000000000000001")
print("Deleted:", deleted)async function deleteTweet(tweetId: string): Promise<boolean> {
const url = new URL("https://api.twitterapis.com/twitter/tweet/delete");
url.searchParams.set("id", tweetId);
const res = await fetch(url, {
method: "POST",
headers: { Authorization: `Bearer ${process.env.TWITTERAPIS_KEY}` },
});
if (!res.ok) throw new Error(`Delete failed: ${res.status}`);
const data = await res.json();
return data.deleted as boolean;
}
const deleted = await deleteTweet("1890000000000000001");
console.log("Deleted:", deleted);Full end-to-end example
A single script that registers a session, posts a tweet, and then deletes it.
#!/usr/bin/env python3
"""
post_and_delete.py -- TwitterAPIs write flow demo
Usage:
export TWITTERAPIS_KEY="your_key"
export X_AUTH_TOKEN="your_auth_token"
export X_CT0="your_ct0"
python3 post_and_delete.py
"""
import os
import requests
API_KEY = os.environ["TWITTERAPIS_KEY"]
AUTH_TOKEN = os.environ["X_AUTH_TOKEN"]
CT0 = os.environ["X_CT0"]
BASE = "https://api.twitterapis.com/twitter"
HEADERS = {"Authorization": f"Bearer {API_KEY}"}
# Cost note:
# session register: one-time setup
# tweet create: $0.0016 per call
# tweet delete: $0.0008 per call
def register_session() -> str:
resp = requests.post(
f"{BASE}/customer/session",
headers=HEADERS,
json={"auth_token": AUTH_TOKEN, "ct0": CT0},
timeout=15,
)
resp.raise_for_status()
data = resp.json()
username = data.get("username") or "(identity challenge -- session still active)"
print(f"Session registered. Account: {username}")
return username
def create_tweet(
text: str,
reply_to: str | None = None,
quote: str | None = None,
) -> dict:
body: dict = {"text": text}
if reply_to:
body["reply_to"] = reply_to
if quote:
body["quote"] = quote
resp = requests.post(
f"{BASE}/tweet/create",
headers=HEADERS,
json=body,
timeout=15,
)
if resp.status_code == 409:
raise RuntimeError("Duplicate tweet or rate conflict (409). Change the text.")
if resp.status_code == 429:
raise RuntimeError("Rate limited (429). Wait before retrying.")
resp.raise_for_status()
data = resp.json()
print(f"Tweet posted: {data['url']}")
return data
def delete_tweet(tweet_id: str) -> bool:
resp = requests.post(
f"{BASE}/tweet/delete",
headers=HEADERS,
params={"id": tweet_id},
timeout=15,
)
resp.raise_for_status()
deleted = resp.json().get("deleted", False)
print(f"Tweet {tweet_id} deleted: {deleted}")
return deleted
if __name__ == "__main__":
register_session()
# Post the tweet
result = create_tweet("Hello from TwitterAPIs.")
tweet_id = result["tweet_id"]
# Clean up
delete_tweet(tweet_id)// post-and-delete.ts
// Usage:
// TWITTERAPIS_KEY=key X_AUTH_TOKEN=tok X_CT0=csrf bun post-and-delete.ts
const API_KEY = process.env.TWITTERAPIS_KEY!;
const AUTH_TOKEN = process.env.X_AUTH_TOKEN!;
const CT0 = process.env.X_CT0!;
const BASE = "https://api.twitterapis.com/twitter";
// Cost note:
// session register: one-time setup
// tweet create: $0.0016 per call
// tweet delete: $0.0008 per call
const H = {
Authorization: `Bearer ${API_KEY}`,
"Content-Type": "application/json",
};
async function registerSession(): Promise<string> {
const res = await fetch(`${BASE}/customer/session`, {
method: "POST",
headers: H,
body: JSON.stringify({ auth_token: AUTH_TOKEN, ct0: CT0 }),
});
if (!res.ok) throw new Error(`Session register failed: ${res.status}`);
const data = await res.json();
const username = data.username || "(identity challenge -- session still active)";
console.log(`Session registered. Account: ${username}`);
return username;
}
async function createTweet(opts: {
text: string;
replyTo?: string;
quote?: string;
}): Promise<{ tweet_id: string; url: string }> {
const body: Record<string, unknown> = { text: opts.text };
if (opts.replyTo) body.reply_to = opts.replyTo;
if (opts.quote) body.quote = opts.quote;
const res = await fetch(`${BASE}/tweet/create`, {
method: "POST",
headers: H,
body: JSON.stringify(body),
});
if (res.status === 409) throw new Error("Duplicate tweet or rate conflict (409).");
if (res.status === 429) throw new Error("Rate limited (429). Wait before retrying.");
if (!res.ok) throw new Error(`Tweet create failed: ${res.status} ${await res.text()}`);
const data = await res.json();
console.log(`Tweet posted: ${data.url}`);
return data as { tweet_id: string; url: string };
}
async function deleteTweet(tweetId: string): Promise<boolean> {
const url = new URL(`${BASE}/tweet/delete`);
url.searchParams.set("id", tweetId);
const res = await fetch(url, { method: "POST", headers: H });
if (!res.ok) throw new Error(`Delete failed: ${res.status}`);
const data = await res.json();
console.log(`Tweet ${tweetId} deleted: ${data.deleted}`);
return data.deleted as boolean;
}
// Main
await registerSession();
const { tweet_id } = await createTweet({ text: "Hello from TwitterAPIs." });
await deleteTweet(tweet_id);Cost math
| Operation | Count | Cost |
|---|---|---|
| Session register | Once (re-register on cookie expiry) | $0.001 |
| Tweet create | Per tweet posted | $0.0016 |
| Tweet delete | Per tweet deleted | $0.0008 |
| 1,000 tweets created | Batch operation | $1.50 |
For comparison, X's own API v2 write access requires a paid developer plan. Here you pay only for what you use, no monthly plan required.
Error handling reference
| Status | Endpoint | Meaning | Action |
|---|---|---|---|
400 | Any | Missing required field or bad value | Check request body and params |
401 | Any | Invalid API key or expired session cookies | Re-check TWITTERAPIS_KEY; re-register session with fresh cookies |
402 | Any | Credit balance exhausted | Top up at twitterapis.com |
409 | tweet/create | Duplicate tweet content or write conflict | Change the tweet text; avoid posting identical content back-to-back |
429 | tweet/create, tweet/delete | Rate limited by X for this session | Back off and retry; X applies per-account write limits |
5xx | Any | Server error | Retry with exponential backoff; contact emma@twitterapis.com if it persists |
Identity challenges on new sessions
When you register a session from an IP address X does not recognize, X may issue a challenge (CAPTCHA or email verification). In that case the username field in the session response will be blank. Complete the challenge in a browser on the same account, then re-register the session. Once the session is trusted, subsequent calls proceed without interruption.
Next steps
Read DMs
Use a registered session to list and read direct message conversations.
Authentication
How bearer token auth works and where to find your API key.
Errors
Full status code reference and retry guidance.
Build a brand monitor
Poll advanced search, dedupe by tweet ID, and surface new mentions on a schedule.
Stream the latest tweets for a keyword
Use advanced_search with product=Latest and a high-water-mark tweet ID to poll new tweets in near real time. Full cURL, Python, and Node samples with cost math.
Wire TwitterAPIs into Claude, Cursor, and Windsurf
Install the TwitterAPIs MCP server in under two minutes and let your AI agent search tweets, fetch profiles, and read timelines without writing a line of glue code.