Skip to Content

X (Twitter) Upload API

This section details how to post text, images and videos to X using the API.

Base URL

https://api.multi-upload-tool.com/api/v1

Authentication

All requests require an API Token in the header: x-api-key: YOUR_API_TOKEN


X credits

X charges for every post published through its API, so X posts are billed in credits:

PostCredits
Text, image or video post1
Post whose text contains a link15

A link is detected with X’s own rules (the official twitter-text library): a URL (https://…) or a bare domain with any real top-level domain, such as example.com, youtu.be/… or site.museum. A missing space after a period can make one: sentence.Next is a link, because .next is a top-level domain. Node.js is not.

Each plan includes a monthly allowance (Basic 50, Pro 150, Scale 300), reset on the 1st of each month (UTC). Beyond it, credits come from packs bought on the Plans page; purchased credits never expire. The credits are debited when the post is published, once per account, and given back if publishing fails. A refunded pack takes back its share of credits. A post published with no credits left fails with Not enough X credits. Retry it once credits are added.

Check the balance

GET /x-credits (scope posts:read) returns the balance, the cost of each kind of post and the packs on sale. includedRemaining is what is left of the monthly allowance, reset each month (UTC); purchased credits never expire. An X post fails when total is short of its cost.

curl https://api.multi-upload-tool.com/api/v1/x-credits \ -H "x-api-key: YOUR_API_TOKEN"
{ "success": true, "data": { "balance": { "monthlyAllowance": 150, "includedRemaining": 112, "purchased": 40, "total": 152 }, "costs": { "post": 1, "linkPost": 15 }, "packs": [ { "name": "X Credits 250", "credits": 250, "price": 9 }, { "name": "X Credits 1000", "credits": 1000, "price": 25 }, { "name": "X Credits 5000", "credits": 5000, "price": 99 } ] } }

Credits of a post

GET /upload/:id on an X post carries xCredits: { spent, refunded, net }. It is null until the job has run. A failed post is refunded; a retry debits again.


Validation & Limits

ConstraintLimitNotes
Post text280 characters (25,000 with x_premium)Counted as X counts them: a link always counts 23, emoji and CJK characters count 2.
Media per post1 video or up to 4 imagesText-only posts are allowed
Image file size5 MB per image, 15 MB per GIFJPG, PNG, WEBP, GIF. A GIF must be posted on its own, without other images.
Video file size1 GBMP4, MOV, WebM. Files over 100 MB must use presigned uploads (x_videos).
Video duration20 minutes (125 with x_premium)
Daily posts50 per account

Source: X API — Media upload 


Duplicate content

X’s developer policy bans posting identical content across several accounts. An X post is refused when, in the last 30 days, another X account of the same owner already published:

  • the same text: case, spaces, punctuation and emoji are ignored. Only captions of at least 20 letters or digits are compared.
  • the exact same media file (SHA-256). A re-encoded video is a different file and is not caught.

The job fails with a clear error and the X credits are given back. A bulk post may target only one X account: the other X accounts of the batch get a per-account error.


Upload a post

Endpoint

POST /upload

Content-Type

multipart/form-data

Body Parameters

ParameterTypeRequiredDescription
accountIdstringYesThe ID of the connected X account.
titlestringYes*The post text. Maximum 280 characters. *Optional when media is attached.
descriptionstringNoAlternative to title for the post text.
photoFile / File[] / URLNoUp to 4 images.
videoFile / URLNoOne video.
x_reply_settingsstringNoWho can reply: everyone (default), following, mentionedUsers, subscribers, verified.
x_made_with_aibooleanNoLabel the post as made with AI.
x_paid_partnershipbooleanNoLabel the post as a paid partnership.
x_alt_textstringNoImages only. Alt text applied to every image. Maximum 1000 characters.
x_tagged_usersstringNoImages only. Up to 10 X usernames, as a JSON array or comma-separated. A tag only shows if the person allows photo tagging.
x_subtitles_urlstringNoVideo only. URL of an SRT or VTT file (max 1 MB) shown as captions.
x_subtitles_languagestringNo2-letter language code of the subtitles. Default EN. Needs x_subtitles_url.
x_poll_optionsstringNoText-only posts (X refuses a poll with media). 2 to 4 choices of max 25 characters, as a JSON array or comma-separated.
x_poll_duration_minutesnumberNo5 to 10080. Default 1440. Needs x_poll_options.
x_community_idstringNoNumeric id of an X Community to post to.
x_share_with_followersbooleanNoWith x_community_id: also show the post to followers.
x_super_followers_onlybooleanNoOnly Super Followers see the post. The account needs X subscriptions enabled.
x_premiumbooleanNoThe account has X Premium: text up to 25,000 characters, videos up to 125 minutes.

Booleans are sent as true / false. An unknown x_* field is refused (400) with x_<field>: <message>.

Example Request

curl -X POST https://api.multi-upload-tool.com/api/v1/upload \ -H "x-api-key: YOUR_API_TOKEN" \ -F "accountId=cmf3k9x2a0001l408q7vbn2hd" \ -F "title=New feature out today" \ -F "video=@clip.mp4"

Text post with a poll

curl -X POST https://api.multi-upload-tool.com/api/v1/upload \ -H "x-api-key: YOUR_API_TOKEN" \ -F "accountId=cmf3k9x2a0001l408q7vbn2hd" \ -F "title=Which feature should we ship next?" \ -F "x_poll_options=Calendar,Analytics,Both" \ -F "x_poll_duration_minutes=1440" \ -F "x_reply_settings=following"

Photo post with alt text

curl -X POST https://api.multi-upload-tool.com/api/v1/upload \ -H "x-api-key: YOUR_API_TOKEN" \ -F "accountId=cmf3k9x2a0001l408q7vbn2hd" \ -F "title=Sunset from the office" \ -F "photo=@sunset.jpg" \ -F "x_alt_text=A warm golden sunset over the ocean" \ -F "x_tagged_users=@jane"

Video post with subtitles

curl -X POST https://api.multi-upload-tool.com/api/v1/upload \ -H "x-api-key: YOUR_API_TOKEN" \ -F "accountId=cmf3k9x2a0001l408q7vbn2hd" \ -F "title=New feature out today" \ -F "video=@clip.mp4" \ -F "x_subtitles_url=https://example.com/clip.srt" \ -F "x_subtitles_language=en" \ -F "x_made_with_ai=true"

Example Response

{ "success": true, "message": "Upload queued successfully", "data": { "uploadId": 789, "status": "pending", "jobId": "job_456" } }

Analytics

X accounts are synced once a day, at night. Because X bills every read, only the posts published through Multi Upload Tool in the last 29 days are read, plus the follower count. Posts published directly on X are not tracked. Views are X impressions, comments are replies, shares are reposts + quotes. See Analytics.