Skip to content

ATTACH IMAGES

Twitter Media Upload API: Attach Images for $0.0008

Updated July 2026

How does the Twitter media upload API work?

You send an image as base64 in a JSON body to POST /twitter/media/upload and get back a media_id, then pass that ID to the tweet create call. On TwitterAPIs that is one request at $0.0008, with no INIT, APPEND and FINALIZE sequence. The upload runs as the X account you registered, and one tweet carries up to four images.

Flat $0.0008 per upload. Publishing the tweet that carries the image is a separate call, priced on the post page.

Per our own API records, 125 endpoints share this one Bearer key and one prepaid balance. Start with the quickstart, compare every price, or connect an agent through the MCP server.

By the numbers

One request instead of a sequence

The upload is the step that usually makes a posting integration take a day rather than an hour.

The two media endpoints and what each one does

Uploading is one call and checking is another. Most image uploads only need the first, because the media_id is ready to attach as soon as it comes back. The status route exists for media that X processes in the background, so you can confirm readiness instead of guessing with a sleep.

POST /twitter/media/upload

Upload a base64-encoded image as the authenticated account and receive a media_id you can attach when creating a tweet. Runs as your own logged-in account. Billed at $0.0008 per call.

ParameterWhereRequiredPurpose
media_databodyYesBase64-encoded image bytes, sent in the JSON request body.

GET /twitter/media/status

Return the processing state X reports for a media_id from Upload Media, so a caller can check it before attaching the id to a tweet. Uploads are image only, and X usually reports no state for an image, so state is usually null. Runs as your own logged-in account. Billed at $0.0008 per call.

ParameterWhereRequiredPurpose
media_idqueryYesNumeric media ID (as a string) returned by media/upload.

Sample responses from the media endpoints

Both responses are small on purpose. The upload hands you one identifier, and the status check tells you whether that identifier is ready. The tabs below show the examples published in our OpenAPI document, so they match what the API reference shows.

{
  "ok": true,
  "media_id": "1899999999999999999"
}
FieldWhat it tells you
media_idThe identifier you pass to media_ids when you create the tweet. Keep it as a string, it is 19 digits long.
stateWhere processing stands. Attach the ID once this reads succeeded.
check_after_secsHow long to wait before asking again, so you poll at the pace X suggests.
progress_percentHow far along processing is, useful when a user is watching a progress bar.

Quickstart: upload an image and attach it to a tweet

An upload belongs to an account, so this needs the same one-time session as any other write. After that the flow is two calls: upload the bytes, then publish a tweet that names the ID you got back. Encode the file as base64 first.

  1. Create a key at signup and register the X account with POST /twitter/customer/session, which is free.
  2. Base64-encode the image. In Python that is base64.b64encode(open(path, "rb").read()).decode().
  3. Send the request below with the encoded string in media_data and read media_id from the response.
  4. Pass that ID in media_ids on the tweet create call to publish the post.
curl -X POST "https://api.twitterapis.com/twitter/media/upload" \
  -H "Authorization: Bearer $TWITTERAPIS_KEY" \
  -H "Content-Type: application/json" \
  -d '{"media_data": "iVBORw0KGgoAAAANSUhEUgAA..."}'

What an image post costs per 1,000

Each image is one upload call, and each tweet is one create call, so the total depends only on how many images a post carries. There is no storage charge and no per-megabyte line. The status check bills like any other read, and an image upload rarely needs it.

ActionPer postPer 1,000 posts
Upload one image$0.0008$0.80
Check processing status$0.0008$0.80
Upload one image and publish the tweet$0.0024$2.40
Upload four images and publish the tweet$0.0048$4.80

Calculated from the biller's per-call rows, one call per image at a fill of exactly 1. The signup credit of $0.50 covers about 208 image posts. See all endpoint prices.

Twitter media upload options compared

The price of the upload itself matters less than the work around it. What separates these options is how many requests a single image takes, which credentials you must hold, and how the bill behaves once the post goes out. Rates are each vendor's published figures.

OptionImage post costRequests per imageCredentials
TwitterAPIs$0.00241 to upload, 1 to postBearer key plus one registered session
Official X API$0.015 per post1 for a simple image, several when chunkedDeveloper app and OAuth user tokens
Outstand$0.007 per post after the included 3,000Handled by their publish callAPI key plus your own X app, $19 per month
Browser automationProxy and compute costsA file input and a page loadA logged-in browser profile

What chunked upload is, and when you avoid it

The first-party media flow was built for large files. You open an upload with INIT, send the bytes in pieces with APPEND, close it with FINALIZE, and then poll until processing ends. That design makes sense for a long video on a bad connection, and it is a lot of state to carry when all you want is one PNG on a post.

Our route takes the image in a single JSON field and returns the ID in the same response. There is no multipart body to assemble and no session state to keep between requests, which also means a serverless function can do it in one invocation.

Image input: what to send and what to expect

The documented input is a base64-encoded image, so plan this endpoint around images. Base64 grows a file by about a third, which is worth remembering when you size request bodies at a proxy or an API gateway. Encode the raw file bytes and leave out any data:image/png;base64, prefix, because the field holds the encoded bytes and nothing else.

  • Four to a tweet. A tweet takes up to four image IDs, passed together in media_ids.
  • Attach soon after upload. Treat a media_id as short-lived and post it in the same job rather than keeping a library of IDs for later.
  • Alt text. The media read endpoint returns ext_alt_text on images that have it, which is useful when you audit an account's accessibility.

Errors you will see on an upload

A 400 usually means the body was not valid base64 or the field name was wrong, so check media_data before you look anywhere else. A 403 points at the account or the session: re-check the session with GET /twitter/customer/session/status and confirm the account can post by hand. A 402 means the balance ran out, and a 429 means you should back off and retry.

A failed upload is safe to retry, unlike a tweet. Nothing is published until you pass the ID to the create call, so an orphaned media_id costs you one call and has no effect on the account. The full error contract is on the REST API guide, and our answer on posting a tweet with an image walks through the two calls end to end.

Upload and post in one script

The whole flow fits in a short script, which is the point of a one-request upload. Read the file, encode it, send it, and pass the returned ID to the create call. The example below does exactly that with no retry logic, so you can see the two calls clearly before you add error handling around them.

For several images, run the upload once per file and collect the IDs in a list, then send them together as a JSON array or a comma-separated string in media_ids. Upload in parallel if the images are large, since each upload is independent, and post only after every ID has come back. If one upload fails, retry that file alone and leave the others in place.

Python
import base64
import os
import requests

BASE = "https://api.twitterapis.com/twitter"
HEADERS = {"Authorization": f"Bearer {os.environ['TWITTERAPIS_KEY']}"}

with open("chart.png", "rb") as handle:
    encoded = base64.b64encode(handle.read()).decode()

upload = requests.post(
    f"{BASE}/media/upload",
    headers=HEADERS,
    json={"media_data": encoded},
    timeout=60,
)
upload.raise_for_status()
media_id = upload.json()["media_id"]

post = requests.post(
    f"{BASE}/tweet/create",
    headers=HEADERS,
    json={"text": "This week's numbers.", "media_ids": media_id},
    timeout=30,
)
post.raise_for_status()
print(post.json()["url"])

Jobs that post images automatically

Most automated image posts come from something that already produces a file on a schedule. The upload is the small last step at the end of a pipeline that renders a chart, a screenshot or a generated image, and the useful question is how to keep that step boring. These are the patterns that come up most, each of which works with one key and one registered session.

Running this for several brands is the same code with a different X session on each call. Our answer on using multiple X accounts with one key covers that, and the API key page explains how keys are issued. Budget by counting images and posts separately, using the table above, because the two are billed as separate calls.

  • Scheduled reports. Render a weekly chart, upload it and post it with a one-line summary. The analytics API can supply the numbers behind the chart.
  • Release announcements. Post a screenshot from your release pipeline, and let the post API carry the changelog link at no extra charge.
  • Generated images. An agent can create an image and publish it through the same two calls, with a person approving the result.
  • Replies with images. Set reply_to on the create call. The upload step does not change, and the engagement API covers the other actions a support bot needs.

Sizing requests and keeping them reliable

Base64 turns every three bytes into four characters, so a 3 MB image arrives as about 4 MB of JSON. Check that your HTTP client, proxy and any serverless platform accept a body of that size, because a gateway limit shows up as a confusing failure well before the request reaches us. Compress screenshots and charts before upload, since they shrink a lot and post just as well.

Set a longer client timeout on the upload than on a normal read, and keep the two calls in the same job so a media_id does not sit unused. The rate limits page has the per-key ceiling, and the quickstart covers the shared auth header if this is your first call to the API.

Why the upload is a separate step from the post

Keeping the file apart from the tweet feels like extra work on the first call, and it pays off in the failure cases. A post is the irreversible public action, while an upload has no visible effect. Doing the upload first means a bad file, an oversized body or a network drop fails before anything reaches the timeline, so you never publish a post that promised an image it does not carry.

It also lets you validate before you commit. Run the upload, confirm the media_id came back, and only then send the text. If a human approves posts in your product, do the upload after approval rather than before it, so the ID is fresh when the post goes out. The same order suits a reply bot, where the image is built from the message being answered.

Upload images from an AI agent

Both endpoints are tools in the MCP server, so an agent that generated an image can upload it and post it without writing any HTTP code. Use the MCP page to connect Claude, Cursor or another client with the same key.

  • twitter_media_upload calls POST /twitter/media/upload
  • twitter_media_status calls GET /twitter/media/status
Install the MCP server
npx -y @twitterapis/mcp@latest

Twitter media upload questions

Base64-encode the file and send it as media_data to POST /twitter/media/upload with your Bearer key. You get back a media_id for $0.0008, which you then pass to POST /twitter/tweet/create in media_ids.

The documented input is a base64-encoded image, so build around images. If a video workflow matters to you, check the API reference before you commit to it, because the media_status route is the only part of this pair that describes asynchronous processing.

No. An upload only stores the image and returns an ID. Nothing appears on the account until you create a tweet that references that ID.

The X account behind the session you registered with POST /twitter/customer/session. The media_id is tied to that account, so post it from the same session.

No. Those are steps in X's chunked flow. Our endpoint takes the whole image in one JSON request and returns the media_id directly.

Up to four. Upload each one separately, collect the IDs, and pass them together in the media_ids field when you create the tweet.

Usually not. Check GET /twitter/media/status only if an attach fails because the media is not ready, or if you are handling something X processes in the background. It returns state, check_after_secs and progress_percent.

Post your first image in two calls

Create a key, register one X session for free, upload the image and publish the tweet.