Skip to main content
RapidDev - Software Development Agency
API AutomationsX (Twitter)

How to Automate X (Twitter) Post Scheduling using the API

X API v2 has no native scheduling endpoint — build your own with a queue and cron that calls POST /2/tweets at the target time. The real story in 2026 is cost: $0.015 per text post or $0.20 per link post on pay-per-use, or $200/month for Basic tier. Media uploads require OAuth 1.0a (not OAuth 2.0) via POST /2/media/upload with a chunked INIT/APPEND/FINALIZE flow.

Need help automating? Talk to an expert
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate5 min read4–6 hoursX (Twitter)Last updated May 2026RapidDev Engineering Team
TL;DR

X API v2 has no native scheduling endpoint — build your own with a queue and cron that calls POST /2/tweets at the target time. The real story in 2026 is cost: $0.015 per text post or $0.20 per link post on pay-per-use, or $200/month for Basic tier. Media uploads require OAuth 1.0a (not OAuth 2.0) via POST /2/media/upload with a chunked INIT/APPEND/FINALIZE flow.

Quick facts about this guide
FactValue
PlatformX (Twitter)
Rate limits50,000 posts/month on Basic; 2,400 posts/day account cap
DifficultyIntermediate
Time required4–6 hours
Last updatedMay 2026

API Quick Reference

Auth

Rate limit

50,000 posts/month on Basic; 2,400 posts/day account cap

Format

SDK

REST only

API overview

Base URLhttps://api.x.com/2

Authentication

  1. 1Create a Project and App at developer.x.com — new apps default to pay-per-use billing after February 2026
  2. 2Enable OAuth 2.0 User Authentication in App settings with callback URL and required scopes
  3. 3For media uploads, also generate OAuth 1.0a keys: API Key, API Secret, Access Token, Access Token Secret (all available in the Developer Portal)
  4. 4Implement the PKCE flow: generate code_verifier, derive code_challenge (SHA-256), redirect user to authorization URL
  5. 5After user grants access, exchange authorization code for access and refresh tokens
  6. 6Store both OAuth 2.0 tokens (for posting) and OAuth 1.0a credentials (for media) securely
typescript
1import os
2import requests
3from requests_oauthlib import OAuth1
4
5# OAuth 2.0 token refresh (for text posts)
6def refresh_oauth2_token(refresh_token, client_id):
7 resp = requests.post(
8 'https://api.x.com/2/oauth2/token',
9 data={
10 'grant_type': 'refresh_token',
11 'refresh_token': refresh_token,
12 'client_id': client_id
13 }
14 )
15 return resp.json() # {'access_token': '...', 'expires_in': 7200}
16
17# OAuth 1.0a auth object for media uploads
18def get_oauth1_auth():
19 return OAuth1(
20 os.environ['X_API_KEY'],
21 os.environ['X_API_SECRET'],
22 os.environ['X_ACCESS_TOKEN'],
23 os.environ['X_ACCESS_TOKEN_SECRET']
24 )

Security notes

  • Store all four OAuth 1.0a credentials as environment variables — never in code or config files
  • OAuth 2.0 access tokens expire in 2 hours; refresh automatically before expiry
  • Media upload (OAuth 1.0a) credentials do not expire but rotate them quarterly
  • Use separate app credentials for separate automation projects to isolate cost tracking
  • The media.write scope is required for media upload alongside OAuth 1.0a

Key endpoints

POST/2/tweets

Publish a tweet. For text-only posts, pass just the text field. For media posts, include media.media_ids with IDs from the media upload flow. Requires user-context OAuth 2.0.

ParameterTypeRequiredDescription
textstringrequiredTweet content, max 280 characters
media.media_idsarrayoptionalArray of media IDs from completed media upload (max 4 images or 1 video/GIF)
reply.in_reply_to_tweet_idstringoptionalIf scheduling a reply, the parent tweet ID

Request

json
1POST https://api.x.com/2/tweets
2Authorization: Bearer USER_ACCESS_TOKEN
3Content-Type: application/json
4
5{
6 "text": "Our weekly product update is live! Here's what's new this week."
7}

Response

json
1{
2 "data": {
3 "id": "1790000000000000001",
4 "text": "Our weekly product update is live! Here's what's new this week."
5 }
6}
POST/2/media/upload

Upload media for attaching to tweets. Requires OAuth 1.0a (not OAuth 2.0). Three-step flow: INIT (declare file size), APPEND (upload chunks), FINALIZE (complete upload). For videos, poll status until processing is done.

ParameterTypeRequiredDescription
commandstringrequiredINIT, APPEND, or FINALIZE
total_bytesintegerrequiredTotal file size in bytes (INIT step)
media_typestringrequiredMIME type: image/jpeg, image/png, video/mp4, image/gif
media_datastringoptionalBase64-encoded chunk data (APPEND step)
segment_indexintegeroptionalChunk index starting at 0 (APPEND step)
media_idstringoptionalMedia ID from INIT (required for APPEND and FINALIZE)

Request

json
1# INIT
2POST https://api.x.com/2/media/upload
3Content-Type: application/x-www-form-urlencoded
4[OAuth 1.0a headers]
5
6command=INIT&total_bytes=204800&media_type=image%2Fjpeg
7
8# Response: {"media_id_string": "710511363345354753", ...}

Response

json
1{
2 "media_id": 710511363345354753,
3 "media_id_string": "710511363345354753",
4 "expires_after_secs": 86400
5}
DELETE/2/tweets/{id}

Delete a scheduled tweet that has already been posted, or clean up test posts. Requires user-context OAuth 2.0 and the tweet must belong to the authenticated user.

ParameterTypeRequiredDescription
idstringrequiredID of the tweet to delete

Request

json
1DELETE https://api.x.com/2/tweets/1790000000000000001
2Authorization: Bearer USER_ACCESS_TOKEN

Response

json
1{
2 "data": {
3 "deleted": true
4 }
5}

Step-by-step automation

1

Build the scheduling queue with a database table

Why: X has no native scheduling API, so you need to store posts with their target publish time and status. A simple database table or JSON file tracks what to post and when. A cron job polls this queue every minute and publishes due posts.

Create a scheduled_posts table with columns: id, text, media_urls (optional), scheduled_at (timestamp), status (pending/published/failed), published_tweet_id, created_at. Your cron job queries for rows where status = 'pending' AND scheduled_at <= now().

curl
1# No direct curl equivalent for the queue use your database
2# Test that your scheduled post will work by posting immediately:
3curl -s -X POST 'https://api.x.com/2/tweets' \
4 -H 'Authorization: Bearer USER_ACCESS_TOKEN' \
5 -H 'Content-Type: application/json' \
6 -d '{"text": "Test post from scheduler"}'

Pro tip: Store container expiry awareness in the queue: if a post has been pending for more than 2 hours without attempting to publish, log a warning. Unlike Instagram, X posts don't expire before sending, but stale items indicate a cron failure.

Expected result: A persistent queue of scheduled posts. The cron job finds all posts with scheduled_at in the past and status = 'pending'.

2

Upload media using the chunked upload flow (OAuth 1.0a required)

Why: If your scheduled posts include images or video, you must upload media before creating the tweet. The media upload endpoint requires OAuth 1.0a specifically — using OAuth 2.0 Bearer returns '403 Not permitted OAuth2 on this endpoint'. Chunked upload is required for all media.

Three-phase upload: INIT declares the total file size and gets a media_id. APPEND sends the file in base64-encoded chunks (5MB per chunk recommended). FINALIZE completes the upload. For videos, after FINALIZE you must poll the processing_info field until state = 'succeeded'.

curl
1# Step 1: INIT
2curl -s -X POST 'https://api.x.com/2/media/upload' \
3 -H 'Authorization: OAuth oauth_consumer_key="...",oauth_token="...",oauth_signature_method="HMAC-SHA1",...' \
4 -d 'command=INIT&total_bytes=204800&media_type=image%2Fjpeg'
5
6# Step 2: APPEND (chunk 0)
7curl -s -X POST 'https://api.x.com/2/media/upload' \
8 -H 'Authorization: OAuth ...' \
9 -d 'command=APPEND&media_id=710511363345354753&segment_index=0&media_data=BASE64_CHUNK'
10
11# Step 3: FINALIZE
12curl -s -X POST 'https://api.x.com/2/media/upload' \
13 -H 'Authorization: OAuth ...' \
14 -d 'command=FINALIZE&media_id=710511363345354753'

Pro tip: Upload media assets ahead of schedule time, not at publish time. If you upload at publish time, video processing can take 30–120 seconds, causing your tweet to post late.

Expected result: A media_id_string is returned after FINALIZE. Use this ID in the tweet creation request. Media IDs are valid for 24 hours.

3

Publish the tweet at scheduled time

Why: The cron job fires every minute and processes all due scheduled posts. For each post, it optionally uploads media (if media_urls are present), then creates the tweet. After posting, it updates the queue record with the published_tweet_id and marks status as published.

Call POST /2/tweets with the text and optionally media.media_ids. Check the response for error 187 (duplicate status) — if you accidentally schedule two identical posts, the second will fail. Log the published tweet ID for reference and update queue status.

curl
1# Text-only post
2curl -s -X POST 'https://api.x.com/2/tweets' \
3 -H 'Authorization: Bearer USER_ACCESS_TOKEN' \
4 -H 'Content-Type: application/json' \
5 -d '{"text": "Your scheduled post text here"}'
6
7# Post with image
8curl -s -X POST 'https://api.x.com/2/tweets' \
9 -H 'Authorization: Bearer USER_ACCESS_TOKEN' \
10 -H 'Content-Type: application/json' \
11 -d '{"text": "Your post with image", "media": {"media_ids": ["710511363345354753"]}}'

Pro tip: Add at least a 2-second gap between publishing multiple posts in the same cron run. X's account-level rate limits apply even within a single batch.

Expected result: Each due post is published and the queue record updated with status 'published' and the tweet ID. Failed posts are marked with their error status for review.

4

Track costs and monitor post performance

Why: On pay-per-use, every post costs real money. A post containing a URL costs $0.20 — 13x more than a text post at $0.015. Tracking costs per session and per day prevents surprises. Reading your own post metrics to confirm publication costs $0.001 per read.

After each post, calculate whether it contained a URL and log the cost. Keep a daily running total. Optionally fetch the published tweet's metrics (impression count, like count) after 1 hour to track initial performance. Alert if daily spend exceeds your budget.

curl
1# Verify tweet was published and get metrics
2curl -s 'https://api.x.com/2/tweets/1790000000000000001?tweet.fields=public_metrics,created_at' \
3 -H 'Authorization: Bearer USER_ACCESS_TOKEN'

Pro tip: Link posts at $0.20 each add up fast — 1,000 link posts/month = $200, the same as Basic tier. If you post more than ~1,333 link posts/month, Basic tier at $200/month is cheaper than pay-per-use.

Expected result: Each post's cost is tracked. Daily totals prevent exceeding budget. Post metrics are available for performance reporting.

Complete working code

Complete tweet scheduler with SQLite queue, media upload support, cost tracking, and cron runner. Run via crontab (every minute) or as a daemon.

Error handling

Cause

Using OAuth 2.0 Bearer token for POST /2/media/upload. Media upload specifically requires OAuth 1.0a.

Fix

Cause

You tried to post the same text content twice. X checks for duplicate status across recent posts.

Fix

Cause

Exceeded monthly write limit (50,000 on Basic) or the 2,400 posts/day account cap.

Fix

Cause

OAuth 2.0 access token expired (2-hour lifetime) or refresh token was revoked.

Fix

Cause

Video upload finalized but processing_info.state returned 'failed'. Typically unsupported codec or resolution.

Fix

Rate limits & throttling

ScopeLimitWindow

Security checklist

  • Store OAuth 1.0a API key, secret, access token, and access token secret in separate environment variables
  • Store OAuth 2.0 access and refresh tokens in environment variables or encrypted secrets store
  • Never log token values — log only token presence (is_set: true/false)
  • Implement daily cost caps with automatic scheduler shutdown when cap is reached
  • Validate media files before queuing — check file size and MIME type to prevent upload failures
  • Never post user-submitted content without moderation — only post from a pre-approved content queue
  • Rotate OAuth 1.0a tokens quarterly even though they do not expire automatically
  • Log every published tweet ID to enable deletion or audit if needed

Automation use cases

Best practices

  • Calculate whether pay-per-use or Basic ($200/month) is cheaper before starting — the break-even point is about 13,333 text posts or 1,000 link posts per month
  • Use the content_publishing_limit check equivalent — read x-user-limit-24hour-remaining on every tweet creation response to track toward the 2,400/day account cap
  • Upload media before scheduled time (at least 1 hour earlier for videos) to decouple media processing delays from post timing
  • Never post duplicate text — use a hash of the post content in your queue to detect and reject duplicates before attempting to post
  • Add randomized timing variation of ±5 minutes to scheduled times to avoid looking like a bot posting at exactly :00 seconds
  • For link posts ($0.20 each), use a URL shortener to reduce tweet character count but note: X counts t.co-wrapped URLs regardless of original length, and cost is based on URL presence not count

Ask AI to help

Copy one of these prompts to get a personalized, working implementation.

ChatGPT / Claude Prompt

I'm building an X (Twitter) post scheduler using the X API v2. I need to: 1) Store posts with scheduled times in a SQLite database, 2) Run a cron job every minute to publish due posts via POST /2/tweets, 3) Handle media uploads with OAuth 1.0a via the INIT/APPEND/FINALIZE chunked upload flow, 4) Track costs at $0.015/text post and $0.20/link post. Please write the complete Python implementation with error handling for rate limits (429) and duplicate status (187).

Lovable / V0 Prompt

Build a content scheduling dashboard for X (Twitter) posts. It should include: a calendar view of scheduled posts, a form to add new posts with text, optional image upload, and scheduled time, status badges (pending/published/failed), a daily cost tracker showing estimated spend vs daily cap, and a toggle to pause/resume the scheduler. Use Supabase as the backend.

Frequently asked questions

Is the X API free tier usable for scheduling automation?

No, not for new developers. The Free tier has been effectively closed since February 2026. New apps default to pay-per-use, where each text post costs $0.015 and each link post costs $0.20. The Free tier also allows only 500 posts/month total and 1 request per 24 hours on most read endpoints, making real automation impossible.

Why does media upload require OAuth 1.0a when everything else uses OAuth 2.0?

X deprecated the v1.1 media upload endpoint on March 31, 2025 and migrated to v2 — but the v2 media upload endpoint still requires OAuth 1.0a signing. This is a known inconsistency in X's API. You need both OAuth 2.0 credentials (for tweet creation) and OAuth 1.0a credentials (for media upload) in the same application.

How much does it cost to post 1,000 tweets per month?

It depends on link vs. text content. 1,000 text-only posts: $15/month on pay-per-use — significantly cheaper than Basic ($200/month). 1,000 posts with URLs: $200/month on pay-per-use — exactly the same as Basic. Mixed content: calculate your text/link ratio and compare. If your posts frequently include links, Basic tier often makes more economic sense.

Can I schedule a post with multiple images?

Yes — upload up to 4 images via the media upload flow and include all 4 media_id_string values in the media.media_ids array when creating the tweet. Note: you cannot mix images and video in the same tweet. Upload all images before the scheduled post time since media IDs expire after 24 hours.

My scheduler posted a tweet with an error — how do I delete it?

Use DELETE /2/tweets/{id} with the user-context OAuth 2.0 token. The tweet ID is returned in the POST /2/tweets response — always log it. Deleting a tweet does not refund the $0.015 or $0.20 post cost. Keep a published_tweet_ids log in your database specifically for cleanup operations.

Can RapidDev help me build the scheduling infrastructure?

Yes. RapidDev has guides for building content scheduling backends with Supabase (for the post queue) and Next.js Server Actions (for the publish triggers). If you're using Lovable or V0, we have step-by-step tutorials for connecting your scheduler to a visual content calendar dashboard.

RapidDev

Need this automated?

Our team has built 600+ apps with API automations. We can build this for you.

Book a free consultation
Matt Graham

Written by

Matt Graham · CEO & Founder, RapidDev

1,000+ client projects delivered. Columbia University & Harvard Business School alumnus, U.S. Navy veteran. About the author →

Want this built for you?

We ship production apps at a fixed price — $13K–$25K, 6–10 weeks, source code yours. You've seen what it takes; we do it every week.

Get a fixed-price quote

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.