TwitterAPIs Docs
Guides

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

  1. Extract auth_token and ct0 from your browser cookies on x.com.
  2. Call POST /twitter/customer/session once to register the session.
  3. Call POST /twitter/tweet/create to post a tweet.
  4. Call POST /twitter/tweet/delete to 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 nameWhat it is
auth_tokenYour session token. Treat it like a password.
ct0CSRF 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:

FieldRequiredDescription
auth_tokenYesThe auth_token cookie value.
ct0YesThe ct0 CSRF cookie value.
user_agentNoBrowser user-agent to send with session requests.
proxy_urlNoOptional 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:

FieldRequiredDescription
textYesTweet text. Max 280 characters for standard accounts.
reply_toNoTweet ID to reply to.
quoteNoTweet 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:

ParameterRequiredDescription
idOne of id / urlNumeric tweet ID, e.g. 1890000000000000001.
urlOne of id / urlFull 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

OperationCountCost
Session registerOnce (re-register on cookie expiry)$0.001
Tweet createPer tweet posted$0.0016
Tweet deletePer tweet deleted$0.0008
1,000 tweets createdBatch 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

StatusEndpointMeaningAction
400AnyMissing required field or bad valueCheck request body and params
401AnyInvalid API key or expired session cookiesRe-check TWITTERAPIS_KEY; re-register session with fresh cookies
402AnyCredit balance exhaustedTop up at twitterapis.com
409tweet/createDuplicate tweet content or write conflictChange the tweet text; avoid posting identical content back-to-back
429tweet/create, tweet/deleteRate limited by X for this sessionBack off and retry; X applies per-account write limits
5xxAnyServer errorRetry 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

On this page