twitterapi.io is an independent third-party service. Not affiliated with X Corp.

Blogtwitter scheduling tool

Twitter (X) Scheduling Tool — A Developer's API Integration Guide

By Sarah Wong4 min read

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. Note: twitterapi.io is read-only by design and doesn't offer write endpoints; scheduling uses X official's tweet-create surface.

01 — Section

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.

02 — Section

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)

- statuspending / 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)

python
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
03 — Section

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).

python
# 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
04 — Section

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.

05 — Section

Side-by-side comparison — 3 paths to schedule a tweet

PathAuthPer-tweet costProgrammaticBest for
X official POST /2/tweets + your schedulerOAuth user-context$0.015 (docs.x.com pricing)yes (custom logic)dev-built scheduling tools, niche features
Existing dashboard tool (Buffer, Typefully)UI accountbundled in tierlimited (their API)non-dev users + plug-and-play UI
TweetDeck (X-owned, free tier)X loginbundled / freenoindividual users, basic scheduling

Two practical patterns: (a) the dev path is X official only — twitterapi.io is read-only; (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.

06 — Section

Operational notes — multi-account + cancellation

Multi-account scheduling: store each user's OAuth tokens encrypted in your DB. Worker looks up the right tokens 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.

07 — Section

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 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.

python
# 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.
08 — Questions

Questions readers ask

Why doesn't twitterapi.io offer scheduled posting?

twitterapi.io is read-only by design — it focuses on the public read surface (tweets, profiles, search) and explicitly doesn't offer write endpoints. Scheduling requires write access, which routes you to X official's tweet-create endpoint with user-context OAuth.

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.

09 — Further reading

Continue

Sources & further reading
More from this series
Build it

Stop reading. Start building.

Starter credits cover real testing on real data. Google sign-in, no card, no application queue.

Get an API key
    Twitter (X) Scheduling Tool — API | TwitterAPI.io