Twitter (X) Scheduling Tool — A Developer's API Integration Guide
A Twitter (X) scheduling tool — queue posts now, send them later — is a foundational SaaS feature for content creators, social-media teams, and brand accounts. Buffer, TweetDeck, Typefully, and others sell this as a polished UI; building the dev version is a queue + cron + the X official tweet API. Useful when you need custom scheduling logic that doesn't fit any existing tool's UI.
This guide walks the integration pattern in Python with runnable code, retry handling, and per-call cost from docs.x.com pricing. The worker can post through X official's tweet-create endpoint or through twitterapi.io's create_tweet_v2, which runs through a logged-in session of your own account; both are shown.
Architecture — queue + cron worker
Three components, each independently testable:
1. Queue (storage) — a database table or queue service holding scheduled tweets. Minimal schema: (id, tweet_text, scheduled_at, status, last_attempt_at, user_id, error_msg).
2. Submission endpoint — your UI or API that lets users add scheduled tweets. Validates content (length, no DM, no protected fields) and inserts the row with status='pending'.
3. Scheduler worker — a cron-fired (every 1-5 min) or queue-consumer process that polls for due rows, posts them via X API, and marks status.
Step 1 — store scheduled tweets
A simple Postgres / SQLite schema covers most production schedulers. For high-volume add Redis or a queue service (SQS, RabbitMQ) for the immediate-due queue layer.
Schema fields:
tweet_text — the body to post (validate ≤280 chars at submission)
scheduled_at — when to post (UTC timestamp)
status — pending / posted / failed / cancelled
last_attempt_at — for retry logic
error_msg — last error if any
user_id — which user's account (for multi-account schedulers)
import sqlite3
from datetime import datetime, timezone
conn = sqlite3.connect("scheduler.db")
conn.execute("""
CREATE TABLE IF NOT EXISTS scheduled_tweets (
id INTEGER PRIMARY KEY AUTOINCREMENT,
tweet_text TEXT NOT NULL,
scheduled_at TEXT NOT NULL,
status TEXT DEFAULT 'pending',
last_attempt_at TEXT,
error_msg TEXT,
user_id TEXT NOT NULL
)
""")
def enqueue(tweet_text: str, scheduled_at: datetime, user_id: str) -> int:
if len(tweet_text) > 280:
raise ValueError("tweet too long")
cur = conn.execute(
"INSERT INTO scheduled_tweets (tweet_text, scheduled_at, user_id) VALUES (?, ?, ?)",
(tweet_text, scheduled_at.isoformat(), user_id),
)
conn.commit()
return cur.lastrowid
Step 2 — the scheduler worker
A cron-fired or always-running worker polls the queue for due rows and posts them via the X API. Cron firing every 1-5 minutes is the typical baseline.
Per docs.x.com/x-api/getting-started/pricing: $0.015 per tweet created. Build retry logic in case of transient failures (429, 5xx); don't infinite-loop on permanent failures (content rejected, account suspended).
# pip install tweepy
import tweepy
from datetime import datetime, timezone
def create_x_client(user_id: str):
"""Replace with your token-lookup logic for this user."""
return tweepy.Client(
consumer_key="YOUR_KEY",
consumer_secret="YOUR_SECRET",
access_token=f"USER_{user_id}_TOKEN",
access_token_secret=f"USER_{user_id}_SECRET",
)
def run_scheduler_pass(conn):
"""One scheduler tick — post all due rows."""
now = datetime.now(timezone.utc)
due = conn.execute(
"SELECT id, tweet_text, user_id FROM scheduled_tweets "
"WHERE status = 'pending' AND scheduled_at <= ?",
(now.isoformat(),),
).fetchall()
for row_id, text, user_id in due:
try:
client = create_x_client(user_id)
resp = client.create_tweet(text=text)
conn.execute(
"UPDATE scheduled_tweets SET status='posted', last_attempt_at=?, error_msg=NULL WHERE id=?",
(now.isoformat(), row_id),
)
print(f"posted {row_id}: {resp.data['id']}")
except tweepy.TooManyRequests:
# Don't mark failed — retry next pass
conn.execute(
"UPDATE scheduled_tweets SET last_attempt_at=?, error_msg='rate_limited' WHERE id=?",
(now.isoformat(), row_id),
)
print(f"rate-limited on {row_id} — will retry next pass")
except Exception as e:
# Permanent failure — mark and don't retry
conn.execute(
"UPDATE scheduled_tweets SET status='failed', last_attempt_at=?, error_msg=? WHERE id=?",
(now.isoformat(), str(e)[:200], row_id),
)
print(f"failed {row_id}: {e}")
conn.commit()
# Cron config (system crontab): every minute
# * * * * * cd /path && python scheduler.py
Step 2b — the same worker on twitterapi.io (/twitter/create_tweet_v2)
Write actions on twitterapi.io run through an authenticated session: you log the account in once with POST /twitter/user_login_v2 ($0.10 per login), passing your X username, email, password and a proxy you supply (a residential or ISP proxy works best), and it returns login_cookies. Store the returned login_cookies and reuse them until the session ends; do not log in on every run. Each post, delete, like, retweet, follow or unfollow is then one call at $0.0006 that takes login_cookies and the same proxy. The call acts as that X account, so X's rules apply to it and X can rate-limit, lock or suspend the account; keep a human pace. The session can stop working and require a new login. The proxy is required on every write call. Billing is per call that returns HTTP 200, and that includes some refusals from X, which come back as HTTP 200 with status: error. Reads are different: they need only an API key and no X account at all.
POST /twitter/create_tweet_v2 takes a JSON body with three required fields: login_cookies, tweet_text and proxy, plus your key in the X-API-Key header. The response has a status field (success or error); mark the row posted only on success. This guide keeps the schedule in your own queue, so cancelling, retrying and reporting stay in one place.
Price, side by side: $0.015 per post on the official API (Post: Create) vs $0.0006 here (0.015 / 0.0006 = 25x), plus the $0.10 login, which is one per session and not one per post. For 1,000 posts that is 1,000 × $0.015 = $15.00 official vs 1,000 × $0.0006 + $0.10 = $0.70 here. A post that contains a URL is $0.200 on the official rate card. For a handful of posts on a fresh login the official side is cheaper in total: 5 posts cost 5 × $0.015 = $0.075 there and 5 × $0.0006 + $0.10 = $0.103 here. The two totals cross at 7 posts per login.
The worker below reads the same scheduled_tweets table as Step 2. It uses one account's session; for a multi-account scheduler, store one login_cookies value per account and look it up by user_id, the way the official worker looks up tokens.
import os, sqlite3, requests
from datetime import datetime, timezone
BASE = "https://api.twitterapi.io"
HEADERS = {"X-API-Key": os.environ["TWITTERAPI_IO_KEY"]}
PROXY = os.environ["X_PROXY"] # a proxy you supply: http://username:password@ip:port
def login() -> str:
"""Log the account in once ($0.10) and return login_cookies. Store and reuse it."""
body = {
"user_name": os.environ["X_USERNAME"],
"email": os.environ["X_EMAIL"],
"password": os.environ["X_PASSWORD"],
"proxy": PROXY,
}
if os.environ.get("X_TOTP_SECRET"):
body["totp_secret"] = os.environ["X_TOTP_SECRET"]
r = requests.post(f"{BASE}/twitter/user_login_v2",
headers=HEADERS, json=body, timeout=120)
r.raise_for_status()
data = r.json()
if data.get("status") != "success":
raise RuntimeError(f"login failed: {data.get('message')}")
return data["login_cookies"]
def create_post(login_cookies: str, text: str) -> dict:
"""One post through the logged-in session. Returns the parsed response."""
r = requests.post(
f"{BASE}/twitter/create_tweet_v2",
headers=HEADERS,
json={"login_cookies": login_cookies, "tweet_text": text, "proxy": PROXY},
timeout=60,
)
r.raise_for_status()
return r.json() # carries "status": "success" or "error"
def run_scheduler_pass_v2(conn, login_cookies: str):
"""One scheduler tick: post all due rows through the session."""
now = datetime.now(timezone.utc).isoformat()
due = conn.execute(
"SELECT id, tweet_text FROM scheduled_tweets "
"WHERE status = 'pending' AND scheduled_at <= ?",
(now,),
).fetchall()
for row_id, text in due:
try:
result = create_post(login_cookies, text)
except requests.RequestException as exc:
# network or HTTP error: leave the row pending and retry next pass
conn.execute(
"UPDATE scheduled_tweets SET last_attempt_at=?, error_msg=? WHERE id=?",
(now, str(exc)[:200], row_id),
)
continue
if result.get("status") == "success":
conn.execute(
"UPDATE scheduled_tweets SET status='posted', last_attempt_at=?, error_msg=NULL WHERE id=?",
(now, row_id),
)
print(f"posted {row_id}")
else:
# the call was answered but the post was not created: do not retry blindly
conn.execute(
"UPDATE scheduled_tweets SET status='failed', last_attempt_at=?, error_msg=? WHERE id=?",
(now, str(result.get("message"))[:200], row_id),
)
print(f"failed {row_id}: {result.get('message')}")
conn.commit()
if __name__ == "__main__":
cookies = os.environ.get("X_LOGIN_COOKIES") or login() # store and reuse the value
run_scheduler_pass_v2(sqlite3.connect("scheduler.db"), cookies)
Step 3 — UI / submission endpoint
Your scheduler product needs a way for users to add posts. Options:
Web form — UI for typing tweet text + picking date/time. Posts to a /enqueue endpoint that validates + inserts.
API endpoint — let other apps schedule on behalf of users (with user auth tokens).
Bulk import — CSV upload for content-marketing teams pre-scheduling a quarter's posts.
Recurrence support — let users say 'every Tuesday at 9am' rather than scheduling each instance manually.
Wire whichever pattern fits your audience. Most schedulers start with the web form + recurrence + bulk import.
Side-by-side comparison — 4 paths to schedule a tweet
Two practical patterns: (a) there are two dev paths, X official with OAuth and twitterapi.io's create_tweet_v2 through your own account session; (b) building your own makes sense when you need custom logic (industry-specific recurrence, integration with your CRM, multi-account agency workflow) that existing tools don't cover.
Operational notes — multi-account + cancellation
Multi-account scheduling: store each user's OAuth tokens encrypted in your DB (or each account's login_cookies on the twitterapi.io route, treated as a secret the same way). Worker looks up the right credentials per row before calling the API.
Cancellation: provide a UI for users to cancel pending posts. Updating status='cancelled' before the cron fires removes the row from due-queue.
Time-zone handling: store all timestamps in UTC; convert to user's TZ only in UI. Avoids the canonical scheduler-bug class (DST shifts, region-specific weirdness).
Failure notifications: when a row hits status='failed', notify the user via email or in-app. They probably want to know their scheduled post didn't go through.
Cost framing + scaling
Math from docs.x.com pricing at $0.015 per tweet created:
10 scheduled posts per day per user = $0.15/day = ~$4.50/mo per user
100 users × 10 posts/day = $15/day = ~$450/mo
1,000 users × 5 posts/day = $75/day = ~$2,250/mo
The same volumes through twitterapi.io at $0.0006 per post: 10 posts per day per user = $0.006/day = ~$0.18/mo, 100 users × 10 posts/day = $0.60/day = ~$18/mo, and 1,000 users × 5 posts/day = $3/day = ~$90/mo, plus $0.10 for each login.
The marginal cost is per-tweet-posted; the platform cost (your DB, scheduler worker, UI hosting) is fixed and small. Scheduler-product economics: bill users a markup on the per-post cost.
# Practical example: scheduler runner — combines queue + worker + cron entrypoint.
import os, sqlite3, sys
from datetime import datetime, timezone
import tweepy
DB_PATH = os.environ.get("SCHEDULER_DB", "scheduler.db")
def get_conn():
conn = sqlite3.connect(DB_PATH)
conn.execute("""CREATE TABLE IF NOT EXISTS scheduled_tweets (
id INTEGER PRIMARY KEY AUTOINCREMENT,
tweet_text TEXT NOT NULL,
scheduled_at TEXT NOT NULL,
status TEXT DEFAULT 'pending',
last_attempt_at TEXT,
error_msg TEXT,
user_id TEXT NOT NULL
)""")
return conn
def run_pass():
conn = get_conn()
now = datetime.now(timezone.utc)
due = conn.execute(
"SELECT id, tweet_text, user_id FROM scheduled_tweets "
"WHERE status = 'pending' AND scheduled_at <= ?",
(now.isoformat(),),
).fetchall()
posted, failed = 0, 0
for row_id, text, user_id in due:
# Replace with your real token lookup per user_id
try:
client = tweepy.Client(
consumer_key=os.environ["X_KEY"],
consumer_secret=os.environ["X_SECRET"],
access_token=os.environ[f"X_USER_{user_id}_TOKEN"],
access_token_secret=os.environ[f"X_USER_{user_id}_SECRET"],
)
resp = client.create_tweet(text=text)
conn.execute(
"UPDATE scheduled_tweets SET status='posted', last_attempt_at=? WHERE id=?",
(now.isoformat(), row_id),
)
posted += 1
except tweepy.TooManyRequests:
conn.execute(
"UPDATE scheduled_tweets SET error_msg='rate_limited', last_attempt_at=? WHERE id=?",
(now.isoformat(), row_id),
)
except Exception as e:
conn.execute(
"UPDATE scheduled_tweets SET status='failed', error_msg=?, last_attempt_at=? WHERE id=?",
(str(e)[:200], now.isoformat(), row_id),
)
failed += 1
conn.commit()
conn.close()
print(f"pass: posted={posted}, failed={failed}")
if __name__ == "__main__":
run_pass()
# Cost framing (math from docs.x.com pricing):
# $0.015 per tweet created. 1,000 daily posts = $15/day = ~$450/mo.
# At scheduler-product scale, mark up per-post cost in your billing layer.Questions readers ask
Can I build the scheduler on twitterapi.io instead of the official API?
Yes. The worker calls POST /twitter/create_tweet_v2 ($0.0006 per post, against $0.015 on the official API under Post: Create) with login_cookies from one POST /twitter/user_login_v2 login ($0.10, one per session) and a proxy you supply. The post is made as your own X account, so the conditions in Step 2b apply. The worker in this guide keeps the schedule in your own queue and posts each row when it is due.
How do existing tools like Buffer + Typefully build this?
Same underlying pattern: queue + cron worker + X official's tweet-create endpoint. Their value-add is the UI, multi-channel support (Instagram, LinkedIn, etc.), and operational reliability. Building your own gives you custom-logic flexibility but you'll re-implement the basics.
What's the smallest viable cron cadence?
1 minute is fine — your worker just polls 'due rows' and posts. Faster cadence buys nothing because tweets don't need second-precision scheduling. Slower cadence (every 5 min) means up to 5-min delay between scheduled time and actual post — usually acceptable.
How do I handle scheduling across time zones?
Store scheduled_at in UTC always. Convert to user's TZ only at UI render time. This avoids the canonical scheduler-bug class around DST shifts, daylight saving, and region-specific quirks.
What if a user revokes OAuth tokens mid-schedule?
Your worker call will return an auth error. Mark the row status='failed' with the error, and notify the user that their token is no longer valid. They'll need to re-authenticate to resume scheduling.
Can I schedule threads, not just single tweets?
Yes — store the thread as a multi-row queue with explicit ordering and a chain reference. The worker fetches all rows in order at posting time and runs the chain pattern. See /blog/how-to-make-twitter-thread-api-tutorial for the thread-chaining details.
Continue
- X API — pricing (docs.x.com, 2026 verified)
- Tweepy documentation
- twitterapi.io docs — user_login_v2
- twitterapi.io docs — create_tweet_v2
- twitterapi.io — pricing
- Twitter (X) API — cluster hub
- How to make a Twitter (X) thread via API
- Rate Limit Exceeded on Twitter (X) — Fixes
- Twitter scheduler — tools roundup
- twitterapi.io pricing
- 01How do you earn TwitterAPI.io credits by writing about the API?
- 02Twitter (X) Lists: What They Are, How to Make One, and How to Use Them
- 03Tweepcred: How Twitter (X) Scores Account Reputation, Read From the Code
- 04Tweet Failed to Send on Twitter (X)? What Each Error Means and How to Fix It
- 05How to Embed a Video on Twitter (X): In a Post or on Your Website
- 06Twitter (X) Hashtag Analytics — API Guide for Devs
- 07Agent Reach and Twitter (X): What the Twitter Channel Does, What It Needs, and the API Route
- 08How to Use Twitter (X) Without an Account — A Developer's API Guide
Stop reading. Start building.
Starter credits cover real testing on real data. Google sign-in, no card, no application queue.
Get an API keyAlready building with TwitterAPI.io? Publish an honest tutorial or build log about it and earn up to $200 a month in API credits.