How to Make a Twitter (X) Thread via API — Developer Tutorial
Twitter (X) threads — a series of replies posted by the same author, forming a connected narrative — are a common content format for long-form posts that don't fit in a single tweet. Posting a thread programmatically lets you templatize a multi-tweet post (newsletter cross-post, long-form analysis, automated thread builder) and run it as part of a workflow.
This guide walks the API workflow with runnable Python: post the root tweet, chain replies via in_reply_to_tweet_id, handle rate limits. Note: twitterapi.io is read-only by design and doesn't offer write endpoints; thread posting uses X official's surface.
How threads actually work on the wire
A thread is a sequence of tweets where each subsequent tweet is a reply to the previous one — same author, chained via in_reply_to_tweet_id. There's no special 'thread' type at the API layer; it's just a sequence of regular POST /2/tweets calls with the reply field set.
Key fields:
- in_reply_to_tweet_id — the previous tweet's ID; chains the new tweet to the thread
- reply.in_reply_to_tweet_id (v2 schema) — same field, nested under reply per the newer endpoint shape
- for_super_followers_only — gate the thread to subscribers (if you have Super Follows enabled)
The thread renders in the X UI as a connected stack of tweets when the author is the only replier; if other users reply mid-stream, the X UI shows them as branches off the main chain.
The thread-posting pattern
Post tweet 1 (root). Capture its ID. Post tweet 2 with in_reply_to_tweet_id=tweet1_id. Capture tweet 2's ID. Post tweet 3 with in_reply_to_tweet_id=tweet2_id. Repeat.
Each new tweet's reply field references the previous tweet, not the root — this is what threads the posts together visually in the X UI.
# pip install tweepy
import tweepy, time
client = tweepy.Client(
consumer_key="YOUR_KEY",
consumer_secret="YOUR_SECRET",
access_token="USER_TOKEN",
access_token_secret="USER_TOKEN_SECRET",
)
def post_thread(tweets: list[str]) -> list[str]:
"""Post a thread; return list of tweet IDs in order."""
if not tweets:
return []
ids = []
# Post root tweet
root = client.create_tweet(text=tweets[0])
ids.append(root.data["id"])
prev_id = root.data["id"]
# Chain replies
for body in tweets[1:]:
resp = client.create_tweet(text=body, in_reply_to_tweet_id=prev_id)
ids.append(resp.data["id"])
prev_id = resp.data["id"]
time.sleep(1.5) # safe pacing
return ids
thread = [
"Thread on Twitter API rate limits 1/4 — what 429 actually means and why X tightened limits in 2024-2026.",
"2/4 — read the rate-limit headers: x-rate-limit-limit, x-rate-limit-remaining, x-rate-limit-reset (Unix timestamp).",
"3/4 — handle 429 with exponential backoff: sleep until reset, retry with jitter. Don't burst.",
"4/4 — for read-heavy workloads, the per-call cost ratio (33×+) at a third-party API can be a structural fix vs upgrading the X tier.",
]
ids = post_thread(thread)
print(f"posted thread: {ids}")
Rate limits + safe pacing
X publishes per-endpoint rate limits at docs.x.com. The tweet-create endpoint has its own per-15-minute cap; consult the current docs.
Safe pacing: 1.5-2 seconds between thread posts is the operational baseline. Faster bursting risks 429s; slower is fine.
Backoff on 429: when a tweet-create returns 429, sleep until the reset header (or 60s if header missing) plus jitter, then retry.
Mid-thread failure: if the chain breaks mid-thread (network error, 429, content rejected), you have a partial thread. Decide your policy: continue from the last successful tweet, retry the failed one, or abort. Log every tweet ID for audit.
Side-by-side comparison — 3 paths to post a thread
Two patterns: (a) the dev path is X official only — twitterapi.io is read-only, so it doesn't offer write endpoints by design; (b) for non-dev users a thread-tool's UI is faster than building from scratch; for dev-built workflows the API path is the only programmatic option.
Content patterns — what makes a good thread
Numbering — '1/N' convention helps readers understand thread length; X UI shows the chain but explicit numbering reduces cognitive load.
Hook in tweet 1 — the root tweet drives whether readers expand the thread. Lead with the strongest claim or question.
One idea per tweet — Twitter UI presents each tweet as a discrete unit; trying to cram multiple ideas across tweet boundaries loses the rhythm.
Closing tweet with CTA — if the thread is promotional or links somewhere, the closing tweet is where most engagement happens. Don't bury the call-to-action mid-thread.
Cost framing + scaling
Math from docs.x.com pricing at $0.015 per tweet created:
- 10-tweet thread = $0.15
- Daily thread post over a month = ~$4.50
- Bulk programmatic thread workflow (100 threads/mo at 8 tweets each) = ~$12
Plus rate-limit overhead — at safe-pacing the bottleneck is throughput, not cost. For most thread-poster workflows the bill is single-digit dollars per month.
# Practical example: production-ready thread poster with retry + audit log.
import os, time, json, random
import tweepy
from datetime import datetime, timezone
client = tweepy.Client(
consumer_key=os.environ["X_CONSUMER_KEY"],
consumer_secret=os.environ["X_CONSUMER_SECRET"],
access_token=os.environ["X_USER_TOKEN"],
access_token_secret=os.environ["X_USER_SECRET"],
)
def post_thread_safe(tweets: list[str], log_path: str = None) -> dict:
log_path = log_path or f"thread_log_{datetime.now(timezone.utc).strftime('%Y%m%d_%H%M%S')}.jsonl"
ids = []
prev_id = None
with open(log_path, "a") as log:
for i, body in enumerate(tweets):
entry = {"index": i, "body": body, "at": datetime.now(timezone.utc).isoformat()}
try:
kwargs = {"text": body}
if prev_id is not None:
kwargs["in_reply_to_tweet_id"] = prev_id
resp = client.create_tweet(**kwargs)
tid = resp.data["id"]
ids.append(tid)
prev_id = tid
entry["tweet_id"] = tid
entry["status"] = "ok"
except tweepy.TooManyRequests:
entry["status"] = "rate_limited"
log.write(json.dumps(entry) + "\n")
time.sleep(60 + random.uniform(0, 5))
# retry once
try:
resp = client.create_tweet(**kwargs)
tid = resp.data["id"]
ids.append(tid)
prev_id = tid
entry["tweet_id"] = tid
entry["status"] = "ok_after_retry"
except Exception as e:
entry["status"] = f"failed_after_retry: {e}"
except Exception as e:
entry["status"] = f"error: {e}"
log.write(json.dumps(entry) + "\n")
time.sleep(1.5 + random.uniform(0.1, 0.5))
return {"ids": ids, "log": log_path}
result = post_thread_safe([
"1/3 — Always log every tweet ID for audit + recovery.",
"2/3 — Retry once on 429 with jitter, then give up to avoid spammy retries.",
"3/3 — Pace 1.5-2s between posts as the safe baseline.",
])
print(result)
# Cost framing (math from cited pricing):
# 3-tweet thread = 3 × $0.015 = $0.045
# Daily thread post for a month = $1.35
# Bulk thread workflow (100 threads × 8 tweets) = $12/moQuestions readers ask
Can I post a thread via twitterapi.io?
No — twitterapi.io is read-only by design. Thread posting requires write access via X official with OAuth user-context auth.
How long can each tweet in a thread be?
Standard tweet length applies — 280 characters per tweet on the basic surface. X premium accounts have longer post limits (4,000+ chars) but each individual tweet in the thread is still subject to the underlying character cap per tweet.
Can other users insert tweets into my thread?
Yes — any user can reply to any tweet, including a tweet in your thread. Their reply appears as a branch off your thread in the X UI but doesn't break your main chain. Your thread renders normally for any reader navigating only your tweet IDs.
What if a mid-thread tweet fails to post?
You have a partial thread. Your audit log should capture every successful tweet ID and the failed one. Decide your policy: retry the failed one (and continue from there with the chain), abandon the thread (publish the partial and notify), or delete and retry from scratch.
Does the order matter in the in_reply_to chain?
Yes — each new tweet should reply to the IMMEDIATELY PREVIOUS tweet in the thread, not to the root tweet. If you accidentally chain all tweets to the root, the X UI will render them as separate replies to the root, not a connected thread.
Can I schedule a thread in advance via API?
X's POST tweet endpoint doesn't have a built-in schedule param. For scheduled threads, run your own scheduler (cron job, queue worker) that calls the API at the scheduled time. Third-party thread tools (Typefully, etc.) offer UI-based scheduling that wraps this same pattern.
Continue
- X API — pricing (docs.x.com, 2026 verified)
- Tweepy documentation
- twitterapi.io — pricing (read-only API)
- Twitter (X) API — cluster hub
- Rate Limit Exceeded on Twitter (X) — Fixes
- Twitter (X) API in Python — complete guide
- X post template — programmatic posting
- twitterapi.io pricing
Stop reading. Start building.
Starter credits cover real testing on real data. Google sign-in, no card, no application queue.
Get an API key