When POST /2/tweets fails, X answers with an HTTP status and a JSON body whose detail names the problem. The status
tells you the kind of failure; the detail tells you which one. Many posting failures are a 403, and the fix depends on
its detail.
Every X fact here comes from X's own documentation, checked on October 26, 2026: its response codes, rate limits, create post reference, manage posts guide, OAuth 2.0 reference, pricing, the media introduction, best practices and chunked upload pages, and X's changelog. Where X's docs don't cover an error, this guide says so.
The short answer
| X answers | What it means | What fixes it |
|---|---|---|
| 401 Unauthorized | The token is missing, wrong, expired or revoked | Refresh the token, or have the person sign in again |
| 403, "duplicate content" | The account already posted this text | Change the text; sending it again won't work |
| 403, "not permitted to perform this action" | X won't take this post from this app for this account | Check the app's access and the token's scopes, then the reply and quote rules |
| A refused reply | The post's author didn't mention or quote your account | Reply by hand in X, or make it a post of its own |
| A refused quote | Quote posts need X's Enterprise plan | Quote it by hand in X, or post it without the quote |
| 403, "video longer than N minutes" | The video is over the posting account's limit | Trim it to 20 minutes, or 125 with X Premium |
| 429 Too Many Requests | A rate limit or usage cap | Wait until x-rate-limit-reset, then retry |
Media state failed |
X couldn't process the file | Re-encode it to X's requirements and upload it again |
| 500, 502, 503 or 504 | A problem on X's side | Check the post didn't go out, then retry with backoff |
401: the token
X's response codes page puts a 401 down to invalid or missing credentials. Check, in order:
- A user token. Posting acts as a person, so the create post reference accepts only user tokens: OAuth 2.0 with the
tweet.write,tweet.readandusers.readscopes, or an OAuth 1.0a user access token. An app-only Bearer token can't post. Uploading media with OAuth 2.0 also needsmedia.write. - The header. Send
Authorization: Bearer <access token>for OAuth 2.0. - Regenerated credentials. If you regenerated the app's keys or tokens in the Developer Console, use the new ones.
- An expired access token. An OAuth 2.0 user access token lasts two hours. Ask for the
offline.accessscope at sign-in and X also issues a refresh token: trade it atPOST https://api.x.com/2/oauth2/tokenwithgrant_type=refresh_tokenfor a new access token, without asking the person again. Withoutoffline.accessthere's no refresh token, and the person signs in again.offline.accesslasts until they revoke your app.
403: read the detail
A 403 means X knows who you are but refused the action. The response codes page lists the usual reasons: the app
doesn't have access to the endpoint, the endpoint needs enrollment or approval (problem type client-forbidden, "App
not enrolled or lacks required access"), the token lacks a scope, or the resource is private or protected. For a post,
the detail tells you which case you're in.
"You are not allowed to create a Tweet with duplicate content."
X refuses a post whose text repeats one the account already posted. X's error reference doesn't list this detail, and X doesn't publish how far back it looks.
- Change the text. Sending the same request again gets the same answer.
- Look for a double send. If an earlier attempt timed out or got a 5xx, it may have posted. Read the account's latest posts before you send it again.
- Vary repeats. A weekly reminder needs different text each week.
"You are not permitted to perform this action."
This is X's general refusal for a post, and X's error reference doesn't explain it either. Work through the causes X does document:
- The app's setup. X's manage posts guide requires a developer App inside a Project in the Developer Console.
- Scopes. The token must carry
tweet.write. Scopes are granted at sign-in, so after you add one, have the person sign in again for a token that has it. - A reply or a quote. Self-serve access refuses most of them; see below.
- More than one cashtag. On self-serve access, a post may carry one cashtag (such as
$TICKER); X rejects more. - Credits or the spending limit. See below.
Replies to other accounts
Since February 23, 2026, self-serve (pay-per-use) apps can reply only when the original post's author has summoned the replying account: mentioned it in that post, or quoted one of its posts. X rejects any other reply. X says the change targets automated, low-quality replies, and that Enterprise access isn't affected. Its docs don't give the error body.
- Reply by hand in X, or make the reply a post of its own.
- X also says further restrictions apply to mentioning or quoting accounts through the API, without listing them.
Quote posts
X's create post reference now opens with a warning: quote-posting with quote_tweet_id requires an Enterprise plan and
isn't available on self-serve (pay-per-use) tiers. X's changelog entry of April 16, 2026 lists quote posts, with
following and likes, as removed from all self-serve tiers. Its docs don't give the error body.
- Quote the post by hand in X, or post without the quote.
"This user is not allowed to post a video longer than N minutes."
The video uploaded, but it's longer than the posting account may post. X checks again when you attach a media_id,
against the posting account's status rather than your API plan: 20 minutes and 8 GB by default, 125 minutes and 16 GB
for X Premium or verified accounts. Trim the video, or post it from an account with the longer limit.
Credits and the spending limit
On pay-per-use, X blocks requests once the credit balance runs out (it can dip slightly below zero first), or once the
billing cycle's spending limit is reached, until you add credits or the next cycle starts. X's pricing page says so but
doesn't publish the response. Flaghand recognizes two answers seen in practice: a 402 with CreditsDepleted, and a 403
whose detail says the account "has reached its billing cycle spend cap"
(how Flaghand handles them). Add credits, or raise the spending limit in the Developer Console.
What each call costs.
429: too many requests
X's response codes page gives 429 for "Rate limit or usage cap exceeded", with problem types rate-limit-exceeded and
usage-capped. Every response carries x-rate-limit-limit, x-rate-limit-remaining and x-rate-limit-reset (a Unix
time). Wait until the reset, then retry; X recommends exponential backoff for 429s.
The posting limits in X's rate limits table:
| Endpoint | Per user | Per app |
|---|---|---|
POST /2/tweets: each post, reply and thread part |
100 per 15 minutes | 10,000 per 24 hours |
POST /2/media/upload/initialize, /append and /finalize |
1,875 per 15 minutes | 180,000 per 24 hours |
GET /2/media/upload: processing status |
1,000 per 15 minutes | 100,000 per 24 hours |
POST /2/media/metadata: alt text |
500 per 15 minutes | 50,000 per 24 hours |
- The lower numbers win. X's manage posts guide gives other figures for the same endpoint: 200 requests per 15 minutes per user, and 300 posts and reposts per 3 hours. Plan for the lowest: 100 per 15 minutes and 300 per 3 hours.
- User limits count every app. The manage posts guide says a user's limit applies across any developer App, so posts the account makes through other tools count too.
- Limits and billing are separate. You can stay within the limits and still spend credits, or hit a limit at no cost.
Media: upload, processing and size
Media goes up before the post: initialize, append the file in segments, finalize, then check its status if finalize
returns processing_info. Each step can fail on its own:
- Segments too large. Keep each appended segment at 5 MB or less; X's maximum is 8 MB. Segments count from 0.
- Processing. The state moves from
pendingtoin_progress, thensucceededorfailed. Waitcheck_after_secsbetween status checks, and attach themedia_idonly aftersucceeded. - A failed video. Check it against X's requirements: H.264 video (High Profile recommended), AAC-LC audio (not HE-AAC) in mono or stereo, 60 fps or less, YUV 4:2:0, progressive scan, no open GOP, square pixels, an aspect ratio between 1:3 and 3:1, and 32x32 to 1280x1024 pixels.
- The category.
media_category(tweet_image,tweet_gifortweet_video) decides which limits apply. A large animated GIF needs chunked upload withtweet_gif. - An expired upload. The upload session expires after
expires_after_secs(86,400 seconds, a day, in X's example). Upload close to posting time.
X's limits, checked at upload and again when the post is created:
| Media | X's limit |
|---|---|
| Photo: JPG, PNG, GIF or WEBP | 5 MB |
| Animated GIF | 15 MB. X recommends at most 1280x1080, 350 frames and 300 million pixels in all |
| Video, most accounts | 0.5 seconds to 20 minutes, up to 8 GB |
| Video, X Premium or verified | Up to 125 minutes and 16 GB |
| Alt text | 1,000 characters |
| Per post | Up to 4 photos, 1 animated GIF or 1 video |
An upload that succeeded doesn't guarantee the post: POST /2/tweets checks the limits again, which is where the
video-length 403 comes from.
500, 502, 503 and 504
These are X's side: wait and retry with backoff, and check X's API status. Before you retry a post, read the account's latest posts. If the first attempt went out, the retry can come back as duplicate content.
Posting through Flaghand
Flaghand's check catches several of these before anyone approves a post: text over the account's limit (280 weighted characters, or 25,000 when X reports a Basic, Premium or PremiumPlus subscription), more than 4 media, a video mixed with photos, a photo over 5 MB or a GIF over 15 MB, alt text over 1,000 characters, and any quote post, even of your own post. For a quote it offers to post it yourself in X or drop the quote; for a thread, you can post the opening quote yourself and let Flaghand post the rest under it (draft rules).
When X refuses an approved post anyway:
- The job shows X's status. Its error reads like
X returned HTTP 403: The request couldn't be completed.Flaghand doesn't store X's error body, because it can echo the post's content. - A 429 or 5xx is tried again two minutes later, up to three tries in all. A thread picks up after its last live post, not from the start.
- A 401 holds the job. The owner reconnects X in Settings, then retries the post in the desk. X logins otherwise renew themselves.
- Running out of credits holds the job too. Flaghand prepays X's API, so you don't buy X credits; once they're topped up, the owner retries the held post in the desk.
- Any other 403 fails the job. Fix the draft and approve it again.
Flaghand's own API answers with its own error codes.