X API: How to Post a Tweet with POST /2/tweets

By Mika ReyesPublished 7 min read

The short answer

To post a tweet with the X API, send POST https://api.x.com/2/tweets with a JSON text field and a user access token: OAuth 2.0 with the tweet.read, tweet.write, and users.read scopes, or OAuth 1.0a. Upload media first and pass its IDs. X charges $0.015 per post and $0.20 per post with a link. Adeli publishes to X with one API key and passes X's fees through at X's rates.

To post a tweet with the X API, you send POST https://api.x.com/2/tweets with a JSON body containing text, authorized as the user you're posting for. That takes a user access token: OAuth 2.0 with the tweet.read, tweet.write, and users.read scopes, or OAuth 1.0a. X bills each call, $0.015 per post and $0.20 per post with a link.

Getting the developer app and tokens is covered in our X API access guide, and the full price list is in X API pricing. This post covers the posting call itself: the request, the media upload, threads, the body fields, and the errors. Every X fact links to X's developer docs, fetched 2026-10-19.

What do you need before you start?

  • An X developer app from the Developer Console, with OAuth 2.0 turned on if you'll use it.
  • Credits in your balance. X is pay-per-use, so requests are blocked when the balance runs out.
  • A user access token for the account you're posting as. The app-only Bearer Token can't create posts.
  • The media.write scope too, if you'll attach images or video.

Which X API keys do I need for posting?

It depends on the auth method. X's Manage Posts guide accepts two kinds of user context for POST /2/tweets:

OAuth 2.0 with PKCE OAuth 1.0a User Context
What you send The user's access token as Authorization: Bearer Your API Key and Secret plus the user's Access Token and Secret, signed per request
Scopes for posting tweet.read, tweet.write, users.read None; the tokens must belong to the posting user
For media Add media.write Same tokens
For long-lived access Add offline.access to get a refresh token Not applicable

For your own account, the Access Token and Secret the console generates are enough. For other people's accounts, each user goes through the OAuth flow, which X API access walks through step by step.

Step 1: Post a text tweet

Send the text as JSON with the user's token.

curl -X POST "https://api.x.com/2/tweets" \
  -H "Authorization: Bearer $USER_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"text": "Shipping notes for v2.4 are up."}'

A success is 201 Created with the new post's ID, per the Create Post reference:

{
  "data": {
    "id": "1840000000000000000",
    "text": "Shipping notes for v2.4 are up.",
    "edit_history_post_ids": ["1840000000000000000"]
  }
}

Store data.id. You need it to reply, build a thread, or delete the post later.

Count characters the way X does. X's character counting guide allows 280 characters, but every URL counts as 23, every emoji as 2, and Chinese, Japanese, and Korean characters as 2 each, so the CJK limit is effectively 140.

Step 2: Upload media and attach it

Upload first, then pass the media ID in media.media_ids. A post takes up to 4 images, 1 GIF, or 1 video.

For an image, one request to POST /2/media/upload does it. Send the file as base64 in JSON or as raw bytes in multipart, with a media_category:

curl -X POST "https://api.x.com/2/media/upload" \
  -H "Authorization: Bearer $USER_ACCESS_TOKEN" \
  -F "media=@chart.png" \
  -F "media_category=tweet_image"

The response's data.id is the media ID. Then post with it:

curl -X POST "https://api.x.com/2/tweets" \
  -H "Authorization: Bearer $USER_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"text": "Signups by week", "media": {"media_ids": ["<MEDIA_ID>"]}}'

Video and large GIFs use the chunked flow, from X's chunked upload guide:

  1. Initialize. POST /2/media/upload/initialize with media_type, total_bytes, and media_category (tweet_video or tweet_gif). It returns the media ID.
  2. Append. POST /2/media/upload/{id}/append once per chunk, with segment_index starting at 0. X recommends chunks of 5 MB or less; 8 MB is the maximum.
  3. Finalize. POST /2/media/upload/{id}/finalize. If the response has processing_info, the video is still processing.
  4. Poll status. GET /2/media/upload with command=STATUS and the media_id, waiting check_after_secs between calls, until the state is succeeded.
Category Size limit
tweet_image 5 MB
tweet_gif 15 MB
tweet_video 20 minutes / 8 GB; 125 minutes / 16 GB for Premium or verified users

The video cap belongs to the posting user, not your developer plan. A video over that user's limit uploads fine and then fails at POST /2/tweets with 403 Forbidden.

Step 3: Reply and build a thread

A thread is a chain of replies. Post the first tweet, then reply to its ID, then reply to the reply:

curl -X POST "https://api.x.com/2/tweets" \
  -H "Authorization: Bearer $USER_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"text": "2/ The biggest change is the new export.", "reply": {"in_reply_to_tweet_id": "1840000000000000000"}}'

Plan for these:

  • Replies to other people need a summons on self-serve. X's Manage Posts guide says a reply is only allowed "if the replying account has been explicitly summoned by the original post's author," by an @mention or a quote. Enterprise isn't subject to the rule.
  • Threads aren't atomic. If the fourth post fails, the first three are already live. Save each ID as you go so you can resume from the last one.

What else can the POST /2/tweets body do?

Polls, reply controls, disclosures, communities, and edits. The fields from the Create Post reference that most products use:

Field What it does Limits
poll A poll with options and duration_minutes 2–4 options of 1–25 characters; 5 to 10,080 minutes
reply_settings Who can reply: following, mentionedUsers, subscribers, or verified
quote_tweet_id Quote another post Enterprise only
community_id Post into an X Community
made_with_ai Discloses AI-generated media
paid_partnership Labels the post as a paid partnership
edit_options.previous_post_id Edits an existing post instead of creating one
media.tagged_user_ids Tags up to 10 users in the media Dropped if the user's photo tagging is off
nullcast Keeps the post off the public timeline

One cashtag per post on self-serve. X rejects a post with more than one $TICKER.

How much does each X API post cost?

Each successful create is billed from your credit balance. From X's pricing page, fetched 2026-10-19:

Call Price
Create a post $0.015 per request
Create a post with a URL $0.200 per request
Delete a post $0.010 per request
Upload media No price listed

Links are the expensive part. A post with a URL costs about 13 times a plain post, and each post in a thread is billed separately, so a 5-post thread with one link costs $0.26. X's page doesn't define what counts as a URL. Adeli's records count a bare domain such as example.com as a link (X API pricing docs), so treat anything that looks like a domain as one. Scenarios at SaaS scale are in our X API pricing breakdown.

What are the rate limits for posting to X?

100 posts per 15 minutes per user and 10,000 per 24 hours per app, per X's rate limits page:

Endpoint Per app Per user
POST /2/tweets 10,000 / 24 hours 100 / 15 min
DELETE /2/tweets/:id Not listed 50 / 15 min
POST /2/media/upload 50,000 / 24 hours 500 / 15 min
Chunked upload endpoints 180,000 / 24 hours 1,875 / 15 min

X's own docs disagree. The Manage Posts guide lists 200 posts per 15 minutes per user and 300 per 3 hours. Read the x-rate-limit-limit, x-rate-limit-remaining, and x-rate-limit-reset headers on every response instead of hard-coding either number. A 429 with error code 88 means you've hit the limit; wait until the reset time.

How do you know it worked?

A 201 with data.id means the post is live. The failures you'll see most:

Response Cause Fix
403 Forbidden A video longer than the posting user's cap Check the user's tier, or trim the video
Reply rejected A reply to someone who didn't mention or quote the account (self-serve only) Reply only when summoned, or move to Enterprise
429, error code 88 Rate limit Wait for x-rate-limit-reset
Requests blocked Credit balance at zero Top up, and set a spending limit in the console

Sources: Manage Posts guide and rate limits.

X also refuses duplicates. Posting the same text the account published recently gets rejected, so vary the text of automated posts.

To delete a post, send DELETE https://api.x.com/2/tweets/{id} with the author's token. X's Manage Posts quickstart says you can only delete posts you authored, and the delete is billed.

Is there a faster way to post to X via API?

Yes, if your users are the ones posting. Posting directly means an X developer app, the OAuth flow, a refresh job for every account, the media upload steps, and resuming broken threads yourself.

Adeli's X API maintains the X developer app, has passed X's review, and refreshes every connected account's tokens, so you hold one API key. A post with a thread is one request to POST /api/v1/posts:

curl -X POST "https://app.tryadeli.com/api/v1/posts" \
  -H "Authorization: Bearer $ADELI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "platform": "x",
    "accountId": "00000000-0000-0000-0000-000000000000",
    "text": "Herons fish at dawn. A thread:",
    "thread": [{ "text": "They stand still for minutes at a time." }]
  }'

How Adeli maps to the X steps above:

  • No separate upload step. Send media inline as { content_type, base64 }, or a video as a public HTTPS { url }: up to 4 images, 1 GIF, or 1 video. Adeli caps X images at 5 MB, GIFs at 15 MB, and video at 512 MB.
  • Threads of up to 24 more posts. X publishes synchronously, so 201 is the finished post. If a later post fails, you get 207 with status: "partial", the failed post's error, and not_attempted for the rest, so you know exactly where to resume.
  • Duplicates come back as 409 duplicate_post, and the 280-character limit is counted the way X counts it, with every link as 23.
  • Deleting is DELETE /api/v1/posts/x/{postId} with the post's providerId.
  • X's fees are passed through at X's published rates, with no markup. X needs a card on file, so until your workspace has one, publishing returns 402 x_billing_required. GET /api/v1/usage/x reports the month's running total.

The same posting API publishes to Instagram, TikTok, YouTube, Facebook, and Bluesky. One X account is one connected account on Adeli's pricing, and your first 3 connected accounts are free.

faq

Frequently asked questions

How much does it cost to post a tweet through the X API?

X charges $0.015 per post and $0.200 per post that contains a URL, from your pay-per-use credit balance. Deleting a post costs $0.010. X doesn't list a price for uploading media. Each post in a thread is billed as its own post.

Can I edit a tweet through the X API?

Yes. POST /2/tweets accepts an edit_options object with the previous_post_id of the post you're editing, and X creates the new version instead of a new post. The response can include edit_history_post_ids, the chain of IDs for that post's edits.

Can I quote a post with the X API?

Not on self-serve access. X's Create Post reference says quote_tweet_id requires an Enterprise plan and isn't available on pay-per-use. Replies work on pay-per-use only when the original author mentioned or quoted the replying account.

Why does my X API post return 403 Forbidden?

A common cause is a video longer than the posting user is allowed. X caps video at 20 minutes and 8 GB by default, or 125 minutes and 16 GB for Premium or verified users, and a successful upload doesn't mean the video can be attached. Check the user's tier before posting long video.

Power your next project
with one social media API

Give your users publishing, scheduling, and analytics through one integration your team can actually maintain.

Try with agents

Start with free credits. No credit card required.