> ## Documentation Index
> Fetch the complete documentation index at: https://docs.xquik.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Twitter media upload API for tweets, replies & DMs

> Upload images, GIFs, and MP4 videos from a connected X account. Get mediaUrl for tweets and replies or mediaId for one direct message attachment. 10 credits.

<Panel>
  <Tabs defaultTabIndex={0} sync={false}>
    <Tab title="200" id="response-x-write-upload-media-200">
      ```json theme={null}
      {
        "object": "x_write_action",
        "id": "12345",
        "writeActionId": "12345",
        "action": "upload_media",
        "status": "success",
        "terminal": true,
        "retryable": false,
        "safeToRetry": false,
        "statusUrl": "/api/v1/x/write-actions/12345",
        "pollAfterMs": null
      }
      ```
    </Tab>

    <Tab title="202" id="response-x-write-upload-media-202">
      ```json theme={null}
      {
        "object": "x_write_action",
        "id": "12346",
        "writeActionId": "12346",
        "action": "upload_media",
        "status": "dispatching",
        "terminal": false,
        "retryable": false,
        "safeToRetry": false,
        "statusUrl": "/api/v1/x/write-actions/12346",
        "pollAfterMs": 2000
      }
      ```
    </Tab>

    <Tab title="400" id="response-x-write-upload-media-400">
      ```json theme={null}
      {
        "error": "missing_idempotency_key",
        "message": "Idempotency-Key is required. Generate one unique key for this write.",
        "charged": false,
        "chargedCredits": "0",
        "retryable": false,
        "safeToRetry": true
      }
      ```
    </Tab>

    <Tab title="401" id="response-x-write-upload-media-401">
      ```json theme={null}
      {
        "error": "unauthenticated",
        "message": "Authentication required. Provide a valid API key or bearer token."
      }
      ```
    </Tab>

    <Tab title="402" id="response-x-write-upload-media-402">
      ```json theme={null}
      {
        "error": "insufficient_credits",
        "message": "Insufficient credits. Top up or subscribe to continue."
      }
      ```
    </Tab>

    <Tab title="403" id="response-x-write-upload-media-403">
      ```json theme={null}
      {
        "error": "account_needs_reauth",
        "message": "X account needs re-authentication. Re-add the account."
      }
      ```
    </Tab>

    <Tab title="404" id="response-x-write-upload-media-404">
      ```json theme={null}
      {
        "error": "account_not_found",
        "message": "X account not found. Connect it first at /dashboard/account?tab=x-accounts."
      }
      ```
    </Tab>

    <Tab title="409" id="response-x-write-upload-media-409">
      ```json theme={null}
      {
        "error": "idempotency_conflict",
        "message": "Idempotency-Key was already used with a different request.",
        "charged": false,
        "chargedCredits": "0",
        "retryable": false,
        "safeToRetry": true
      }
      ```
    </Tab>

    <Tab title="413" id="response-x-write-upload-media-413">
      ```json theme={null}
      {
        "error": "media_too_large",
        "message": "This image stays over X's size limit after shrinking. Send a smaller one."
      }
      ```
    </Tab>

    <Tab title="415" id="response-x-write-upload-media-415">
      ```json theme={null}
      {
        "error": "unsupported_media_type",
        "message": "Xquik couldn't read this image. Send a valid image file."
      }
      ```
    </Tab>

    <Tab title="422" id="response-x-write-upload-media-422">
      ```json theme={null}
      {
        "error": "x_rejected",
        "message": "X rejected this request. Check what you sent & the account on x.com before you try again."
      }
      ```
    </Tab>

    <Tab title="429" id="response-x-write-upload-media-429">
      ```json theme={null}
      {
        "error": "rate_limit_exceeded",
        "message": "Too many requests. Try again later.",
        "retryAfter": 60
      }
      ```
    </Tab>

    <Tab title="500" id="response-x-write-upload-media-500">
      ```json theme={null}
      {
        "error": "x_write_failed",
        "message": "Write action failed unexpectedly. Contact support if this persists."
      }
      ```
    </Tab>

    <Tab title="503" id="response-x-write-upload-media-503">
      ```json theme={null}
      {
        "error": "write_tracking_unavailable",
        "message": "Write tracking unavailable. Try again.",
        "charged": false,
        "chargedCredits": "0",
        "retryable": true,
        "safeToRetry": true
      }
      ```
    </Tab>
  </Tabs>
</Panel>

<blockquote className="agent-llms-directive">
  For the complete documentation index, see <a href="/llms.txt">llms.txt</a>.
</blockquote>

<Callout icon="coins" color="#5c3327">
  **10 credits per call** · [All plans](https://xquik.com/#pricing) from \$0.00012/credit
</Callout>

Call `POST /x/media`.
The route accepts one local file or hosted HTTPS media URL.
Each request uses one connected X account.
A completed upload returns a media ID and reusable `mediaUrl`.

Skip this endpoint when a tweet already has public HTTPS media URLs. Call
[Create Tweet](/api-reference/x-write/create-tweet) directly with those URLs.

## Use the Twitter API upload media workflow

Send `multipart/form-data` for a file. Send `application/json` for a hosted
URL. Authenticate with an API key or OAuth bearer token. Add one
`Idempotency-Key` per intended upload. Any HTTP client works.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://xquik.com/api/v1/x/media \
    -H "x-api-key: xq_YOUR_KEY_HERE" \
    -H "Idempotency-Key: media-upload-1895432178065391234" \
    -F "account=myxhandle" \
    -F "file=@/path/to/image.png" | jq
  ```

  ```javascript Node.js theme={null}
  const form = new FormData();
  form.append("account", "myxhandle");
  form.append("file", new Blob([fileBuffer]), "image.png");

  const response = await fetch("https://xquik.com/api/v1/x/media", {
    method: "POST",
    headers: {
      "x-api-key": "xq_YOUR_KEY_HERE",
      "Idempotency-Key": "media-upload-1895432178065391234",
    },
    body: form,
  });
  if (!response.ok) {
    throw new Error(await response.text());
  }
  const { mediaId, mediaUrl } = await response.json();
  ```
</CodeGroup>

### Upload a hosted media URL

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://xquik.com/api/v1/x/media \
    -H "x-api-key: xq_YOUR_KEY_HERE" \
    -H "Idempotency-Key: media-url-1895432178065391234" \
    -H "Content-Type: application/json" \
    -d '{"account": "myxhandle", "url": "https://example.com/image.png"}' | jq
  ```
</CodeGroup>

Both inputs return the same lifecycle record. Poll `statusUrl` after 202. Store
`mediaId`, `mediaUrl`, the account, and the idempotency key.

## Handle media types, large videos & multiple images

Supported media files include AVIF, GIF, JPEG, PNG, WebP, and MP4.
Match the content type to the file. Use a public URL.
Xquik rejects private and reserved addresses. The download timeout is 30 seconds.
The file limit is 15,728,640 bytes.
Xquik shrinks images over 5 MiB to fit X. GIFs and videos go to X unchanged.
An unreadable image returns `415 unsupported_media_type`.
An image Xquik can't shrink enough returns `413 media_too_large`.

Set `is_long_video` to `true` for MP4 files longer than 140 seconds.
Xquik handles the chunked upload and media category.
Send one request, then poll its lifecycle.

For multiple images, call once per file. Collect up to 4 `mediaUrl` values.
Send them together in Create Tweet. DMs accept exactly 1 media ID. Schedulers
store `mediaUrl` until send time. This endpoint does not schedule tweets.

## Store the Twitter API media upload receipt

| Output | Next request | Rule |
| - | - | - |
| `mediaUrl` | Tweet or reply `media` | Send up to 4 image URLs or 1 MP4 URL up to 100 MB. |
| `mediaId` | DM `media_ids` | Send exactly 1 media ID. |

Store the account, success, source reference, and idempotency key with each
receipt. For replies, add `reply_to_tweet_id` in Create Tweet.
Never send `media_ids` to Create Tweet. Use public `mediaUrl` values.
Tweet and reply writes cost 30 credits, plus media surcharges.
DM writes cost 10 credits.

## Fix Twitter media upload API errors

Fix invalid fields or content types after 400. Replace credentials after 401.
Add credits after 402. Reconnect the X account after 403. Connect a missing
account after 404. Keep the original action after 409. Replace unreachable or
private URLs after `422 media_download_failed`. Replace rejected media after
other 422 errors. Honor `Retry-After` after 429. Check `safeToRetry` after 500
or 503.

## Headers

<ParamField header="x-api-key" type="string" required>
  Your API key. OAuth bearer authentication is also supported. Generate a key from the [dashboard](https://xquik.com/dashboard).
</ParamField>

<ParamField header="Idempotency-Key" type="string" required>
  Unique key for this intended write. Reuse it only for an exact network replay.
</ParamField>

<ParamField header="Content-Type" type="string" required>
  Use `multipart/form-data` when uploading a file. Use `application/json` when providing a URL.
</ParamField>

## Body

<ParamField body="account" type="string" required>
  A connected X username or account ID. The account performs the upload.
</ParamField>

<ParamField body="file" type="binary">
  Required without `url`. Accepts AVIF, GIF, JPEG, PNG, WebP, or MP4.
</ParamField>

<ParamField body="url" type="string">
  Required without `file`. Provide a public HTTPS URL. AI agents and MCP clients can send URLs instead of binary uploads.
</ParamField>

<ParamField body="is_long_video" type="boolean">
  Multipart MP4 uploads only. Set `true` when the video exceeds 140 seconds. Defaults to `false`.
</ParamField>

## Response

<Tabs>
  <Tab title="404 Account not found">
    Connect the requested account, then submit a newly approved write.
  </Tab>
</Tabs>

## Durable write recovery

<Warning>
  Send one unique `Idempotency-Key` per intended write.
  Replay the same account, target, payload, and media after a lost response.
  Keep the original key for that replay.
</Warning>

1. Store `id`, the nested `hash` in `request`, `billing`, and `statusUrl`.
2. Poll after `Retry-After` or `pollAfterMs` when `terminal` is `false`.
3. Retry only when `safeToRetry` is `true`.
4. Use a new key when `nextAction.requiresNewIdempotencyKey` is `true`.

### 200 terminal or 202 active

* After HTTP `200`, store the result and settled billing.
* After HTTP `202`, poll the same action. Never submit another write.
* After HTTP `400`, fix the named field. Use a new idempotency key.
* After HTTP `401`, fix authentication. Do not retry unchanged.
* After HTTP `402`, fund the account before another write.
* After HTTP `403`, reconnect the account.
* After HTTP `409`, keep the original action. Use a new key for new input.
* After HTTP `422`, fix the rejected request before retrying.
* After HTTP `429`, wait for `Retry-After`. Follow `nextAction`.

See [Get Write Action Status](/api-reference/x-write/get-write-action-status)
for every lifecycle field, terminal state, billing field, and retry rule.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.