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

How to post a tweet with the X API: the POST /2/tweets request, user tokens and scopes, media upload, threads, per-post fees, rate limits, and the errors.

Canonical page: https://www.tryadeli.com/blog/post-to-x-via-api
Last updated: 2026-10-19

By Mika Reyes, Co-founder, Adeli. Published 2026-10-19.

## 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](https://www.tryadeli.com/blog/x-api-access), and the full price list is in [X API pricing](https://www.tryadeli.com/blog/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](https://docs.x.com/x-api/posts/create-post), fetched 2026-10-19.

## What do you need before you start?

- **An X developer app** from the [Developer Console](https://console.x.com), 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](https://docs.x.com/x-api/posts/manage-tweets/integrate) 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](https://www.tryadeli.com/blog/x-api-access) walks through step by step.

## Step 1: Post a text tweet

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

```bash
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](https://docs.x.com/x-api/posts/create-post):

```json
{
  "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](https://docs.x.com/resources/fundamentals/counting-characters) 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`](https://docs.x.com/x-api/media/upload-media) does it. Send the file as base64 in JSON or as raw bytes in multipart, with a `media_category`:

```bash
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:

```bash
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](https://docs.x.com/x-api/media/quickstart/media-upload-chunked):

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:

```bash
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](https://docs.x.com/x-api/posts/manage-tweets/integrate) 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](https://docs.x.com/x-api/posts/create-post) 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](https://docs.x.com/x-api/getting-started/pricing), 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](https://www.tryadeli.com/docs/pricing/x)), so treat anything that looks like a domain as one. Scenarios at SaaS scale are in our [X API pricing breakdown](https://www.tryadeli.com/blog/x-api-pricing).

## 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](https://docs.x.com/x-api/fundamentals/rate-limits):

| 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](https://docs.x.com/x-api/posts/manage-tweets/integrate) and [rate limits](https://docs.x.com/x-api/fundamentals/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](https://docs.x.com/x-api/posts/manage-tweets/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](https://www.tryadeli.com/product/x) 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`](https://www.tryadeli.com/docs/api/posts/publish-x):

```bash
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}`](https://www.tryadeli.com/docs/api/posts/delete-x-post) 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](https://www.tryadeli.com/social-media-posting-api) publishes to Instagram, TikTok, YouTube, Facebook, and Bluesky. One X account is one connected account on [Adeli's pricing](https://www.tryadeli.com/pricing), and your **first 3 connected accounts are free**.

## 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.

## Related Adeli pages

- [X (Twitter) API for Developers](https://www.tryadeli.com/product/x.md): Publish, schedule, and read analytics for X (Twitter) through one API. Adeli owns the X developer app, token refresh, and review process. Free to start.
- [Social Media Posting & Scheduling API](https://www.tryadeli.com/social-media-posting-api.md): Adeli's social media posting and scheduling API publishes to Instagram, TikTok, YouTube, Facebook, X, and Bluesky in one request, now or at a set time. Free to start.
- [Adeli Pricing — priced per connected account](https://www.tryadeli.com/pricing.md): Your first 3 connected accounts are free, then $5 each for accounts 4–10, $3 each for 11–50, $2 each for 51–100, and $1 each up to 2,000. Above that, talk to sales.
- [All posts on the Adeli blog](https://www.tryadeli.com/blog.md)

## About the author

Mika Reyes is the CEO behind Adeli and a tech and AI content creator. She was previously co-founder and CEO of Parallax, which was acquired by Phantom, and a product lead at LinkedIn. She is a Forbes 30 Under 30 honoree.
