Schedule tweets with an API
Schedule posts on X without a scheduler app.
Pass a time. We hold the post, publish it through X's API when it's due, retry if X is down, and tell your webhook when it's live.
The short answer · Updated Nov 1, 2026
To schedule a post on X (Twitter) programmatically, send it to POST /v1/posts with scheduled_at set to an ISO-8601 time up to 365 days ahead. It's checked against X's 280-character weighted limit when you create it, held until the time, then published through X's API. X's per-post fee applies only when it actually goes out.
X: live. Posting to X works on the API today.
How does scheduling a tweet through the API work?
X's API creates posts immediately; there is no future publish time on POST /2/tweets. A scheduler stores the post and calls X at the right moment. With us, that's one field: add scheduled_at to the request and we store the post with status scheduled.
Our delivery worker runs every minute. When the time comes, it checks the X account is still connected, refreshes the OAuth token if it's close to expiring, uploads any images, creates the post on X and reads it back to confirm it exists. The result goes to your webhook and to GET /v1/posts/:id.
If X is unavailable or rate-limits the request, the attempt is retried after 30s, 2m, 10m, 30m and 2h. After six attempts the target is failed with the last error. A post scheduled for 09:00 during an X outage can therefore go out late; check attempts and published_at on the target to see when it actually went out.
A worked example: schedule a launch post
This schedules one X post with a link for 9:00 New York time. The text is 131 characters as JavaScript counts them but 120 weighted characters as X counts them, because the 34-character URL counts as 23.
curl https://socialsforai.com/v1/posts \
-H "Authorization: Bearer $SOCIALS_API_KEY" \
-H "Idempotency-Key: launch-v2-4-x" \
-d '{
"accounts": ["acc_x_company"],
"text": "v2.4 is out: cancel scheduled posts from the API, 24h webhook retries, custom Bluesky PDS hosts. https://example.com/changelog/v2-4",
"scheduled_at": "2026-11-10T09:00:00-05:00"
}'What comes back when you schedule a tweet?
A 201 with status: "scheduled" and the time normalized to UTC (trimmed here to the relevant fields). Because the text contains a link, the response also carries an x_link_cost warning. It doesn't block the post; it tells you this post will be billed by X at the with-URL rate, $0.200 instead of $0.015.
{
"id": "post_c8Wq1NfZ4yHs6TbA",
"object": "post",
"status": "scheduled",
"scheduled_at": "2026-11-10T14:00:00.000Z",
"targets": [
{
"id": "tgt_P0xEo7Jr2mLa9VuK",
"account_id": "acc_x_company",
"platform": "x",
"status": "pending",
"attempts": 0,
"next_attempt_at": "2026-11-10T14:00:00.000Z",
"url": null,
"error": null
}
],
"warnings": [
{
"account_id": "acc_x_company",
"platform": "x",
"severity": "warning",
"code": "x_link_cost",
"message": "Posts with a URL cost $0.200 through the X API instead of $0.015 (docs.x.com pricing).",
"fix": "Remove the link, or send a link-free version for X in platform_options.x.text. A link in a reply is also billed at the with-URL rate."
}
]
}How do I schedule a batch of tweets?
Send one request per post, each with its own scheduled_at and idempotency key. Here is a week of daily posts from a Python list. If the script dies halfway and you rerun it, the keys make the already-created posts come back as replays instead of duplicates.
Two limits shape batch loads. The fair-use cap of 100 posts per X account counts posts created for that account in the last 24 hours, scheduled ones included, so loading a month of hourly posts in one go stops at 100 with daily_cap_reached; load a few days at a time. And on the Free plan, every created post counts toward the 100-posts-a-month limit when it's created.
import os, requests
from datetime import datetime, timedelta, timezone
API = "https://socialsforai.com"
headers = {"Authorization": f"Bearer {os.environ['SOCIALS_API_KEY']}"}
start = datetime(2026, 11, 10, 14, 0, tzinfo=timezone.utc)
for day, text in enumerate(daily_tips): # 7 different strings
when = (start + timedelta(days=day)).isoformat()
r = requests.post(f"{API}/v1/posts",
headers={**headers, "Idempotency-Key": f"tips-{when}"},
json={"accounts": ["acc_x_company"], "text": text, "scheduled_at": when})
if r.status_code == 422:
for issue in r.json()["error"]["issues"]:
print(day, issue["code"], issue["fix"])
else:
r.raise_for_status()Common mistakes when scheduling tweets
Most scheduling bugs are about time zones, duplicates and counting.
- Timestamps without a time zone.
2026-11-10T09:00:00is rejected with a 400validation_error. AddZfor UTC or an offset like-05:00. - Times slightly in the past. A
scheduled_atup to 5 minutes ago publishes immediately; more than 5 minutes ago fails withscheduled_in_past. Clock skew on your server is the usual cause. - Recycled text. Two posts with identical text for the same account, created within 24 hours of each other, fail with
duplicate_content, even if they're scheduled a week apart. - Counting with
len()or.length. X weights characters. Check with the X character counter ordry_run, and read how the weights work on the X character limit page. - Editing by resending. There is no edit. Cancel and recreate with a new idempotency key; the old key with a new body returns 409
idempotency_key_reused.
How do I troubleshoot a scheduled tweet that didn't post?
Call GET /v1/posts/:id and read the target's status, attempts, next_attempt_at and error. The error.code tells you what happened.
- Status
pendingwith attempts above 0 and anext_attempt_at: a retryable failure (platform_rate_limited,platform_unavailable). It's waiting for the next retry; do nothing. account_needs_reauth: X refused the token and it couldn't be refreshed. Send the account owner a new connect link, then recreate the post.duplicate_contentfrom X: X itself rejected the text as a duplicate. Change the text and create a new post.platform_rejected: X refused the post for another reason; its response is in the message.- Status
canceled: someone calledDELETE /v1/posts/:id. Nothing was sent to X and nothing was charged.
Should you use X's own scheduler instead?
If a person schedules their own posts by hand, yes. X's own scheduling tools show the post as it will look, with no API work. Paying for an API makes no sense for that workflow.
An API is the better fit when software decides what goes out: a product that posts for its users, a pipeline that announces releases, a Twitter bot, or an agent using our X MCP server. It's also the only way to schedule the same announcement to X, Bluesky and LinkedIn in one call; see crossposting. X's per-post fees still apply; see X API pricing.
How to schedule tweets with an API
- 01
Get a key and connect X
POST https://socialsforai.com/v1/workspaces for an API key, then POST /v1/connect-links with platforms set to x. The account owner opens the link and approves on X. You get an account id.
- 02
Write the post and check it
Send it with dry_run: true. Preflight counts the text the way X does (URLs 23, emoji and CJK 2) and returns text_too_long with the overage if it doesn't fit.
- 03
Add scheduled_at
Set scheduled_at to a UTC time like 2026-11-10T14:00:00Z, or a local time with an offset like 2026-11-10T09:00:00-05:00. Up to 365 days ahead.
- 04
Send it with an idempotency key
POST /v1/posts with an Idempotency-Key header. The response is 201 with status scheduled. A network retry with the same key returns the same post instead of a second one.
- 05
Track or cancel it
A post.published or post.failed webhook fires when it's done. Before then, DELETE /v1/posts/:id cancels it and nothing is sent to X.
Questions
What developers ask
Is a scheduled post charged if I cancel it?
No. Nothing is sent to X until the scheduled time, so a post canceled with DELETE /v1/posts/:id before then never reaches X and X charges nothing. Canceling works until the post starts publishing; after that the API returns 409 post_not_cancelable.
How far in advance can I schedule tweets with the API?
Up to 365 days. A scheduled_at further out fails preflight with scheduled_too_far, and one more than 5 minutes in the past fails with scheduled_in_past. X OAuth tokens refresh automatically, so a post scheduled months ahead doesn't need the account to reconnect, unless the owner revokes access.
How accurate is the scheduled time?
Our delivery worker runs every minute, so a post normally goes out within about a minute after its scheduled_at. If X returns a rate limit or server error, the attempt is retried after 30 seconds, 2 minutes, 10 minutes, 30 minutes and 2 hours, so an outage can delay a post. The target's attempts and published_at fields show what happened.
Can I schedule a tweet with images?
Yes. Add up to 4 images in the media array as public https URLs, at most 5 MB each, with optional alt text. The images are fetched and uploaded to X when the post publishes, not when you schedule it, so keep the URLs reachable until the scheduled time. Video isn't supported yet.
How many tweets can I schedule per day?
Our fair-use cap is 100 posts per X account in a rolling 24 hours, counted when posts are created, scheduled ones included. X's own rate-limit docs list POST /2/tweets at 100 requests per 15 minutes per user. For long queues, create posts a few days at a time.
Can I schedule a thread?
Not as a thread. The API creates standalone posts and has no reply-to field yet, so you can schedule several posts a minute apart, but they won't be linked as replies on X. Each post in such a sequence needs its own distinct text, or the later ones fail preflight with duplicate_content.
Get an API key and send your first post in five minutes.