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

# Upload media for tweets and DMs

> Upload media by file or URL, then use mediaUrl for tweets or replies and mediaId for one media attachment in DMs.

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

Use this workflow when a product, support, CRM, or AI agent system needs a hosted media URL for tweet posts or an uploaded media ID for direct messages. Xquik accepts local files with `multipart/form-data` and HTTPS media URLs with `application/json`.

If you already have public image URLs or a public MP4 video URL for a tweet or reply, skip `POST /x/media` and pass those URLs directly in the `media` array on `POST /x/tweets`. Send up to 4 images or exactly 1 MP4 video up to 100 MB. Use `POST /x/media` when you need Xquik to host a local file, validate a generated media URL, or produce a `mediaId` for a DM attachment.

<Callout icon="video" color="#2563eb">
  **Need to post an MP4 tweet?** If the MP4 is already a public HTTPS URL, skip upload and call [Create Tweet](/api-reference/x-write/create-tweet) with `media: ["https://example.com/video.mp4"]`. Use `POST /x/media` first only when Xquik must host a local file or validate a generated URL, then pass the returned `mediaUrl` to `POST /x/tweets`.
</Callout>

## When to use this workflow

<CardGroup cols={2}>
  <Card title="Upload a local file" icon="paperclip">
    Upload a local image, GIF, or MP4 with `POST /x/media` and `multipart/form-data`.
  </Card>

  <Card title="Upload generated media" icon="image">
    Upload AI-generated media from a URL with `POST /x/media` and `application/json`.
  </Card>

  <Card title="Post tweets with public media URLs" icon="send">
    Pass the URLs directly in the `media` array on `POST /x/tweets`.
  </Card>

  <Card title="Post tweets with media uploaded through Xquik" icon="image">
    Pass returned `mediaUrl` in the `media` array on `POST /x/tweets`.
  </Card>

  <Card title="Post tweet replies with uploaded media" icon="message-square">
    Pass `mediaUrl` plus `reply_to_tweet_id` on `POST /x/tweets`.
  </Card>

  <Card title="Send a DM with uploaded media" icon="message-square">
    Pass returned `mediaId` as the only item in `media_ids` on `POST /x/dm/{userId}`.
  </Card>
</CardGroup>

## Data you get

<CardGroup cols={2}>
  <Card title="DM attachment ID" icon="paperclip">
    `mediaId` is the uploaded media ID for one-item DM `media_ids` arrays. Media IDs are valid for 24 hours after upload.
  </Card>

  <Card title="Tweet media URL" icon="image">
    `mediaUrl` is the public media URL for tweet `media` arrays.
  </Card>

  <Card title="Upload confirmation" icon="circle-check">
    `success` is `true` after upload completes.
  </Card>

  <Card title="Tweet confirmation" icon="send">
    `tweetId` is returned by `POST /x/tweets` after the media tweet or reply is confirmed.
  </Card>
</CardGroup>

## End-to-end media handoff

Use one checkpoint object after upload and the downstream tweet, reply, or DM
write. Keep the field boundary explicit: tweets and replies use public
`mediaUrl` values in `media`; DMs use the uploaded `mediaId` as the only
`media_ids` item.

```json theme={null}
{
  "workflow": "media_upload_handoff",
  "upload": {
    "endpoint": "/api/v1/x/media",
    "account": "myxhandle",
    "source_type": "url",
    "source": "https://example.com/image.png",
    "media_id": "1893726451023847424",
    "media_url": "https://media.xquik.com/uploads/1893726451023847424.png",
    "media_id_expires_in_hours": 24,
    "success": true
  },
  "tweet_post": {
    "endpoint": "/api/v1/x/tweets",
    "account": "myxhandle",
    "media": ["https://media.xquik.com/uploads/1893726451023847424.png"],
    "tweet_id": "1895432178065391234",
    "success": true,
    "charged_credits": "32"
  },
  "reply_post": {
    "endpoint": "/api/v1/x/tweets",
    "account": "myxhandle",
    "reply_to_tweet_id": "1893704267862470862",
    "media": ["https://media.xquik.com/uploads/1893726451023847424.png"],
    "tweet_id": "1895432178065391235",
    "write_status": "posted"
  },
  "dm_send": {
    "endpoint": "/api/v1/x/dm/44196397",
    "account": "myxhandle",
    "recipient_user_id": "44196397",
    "media_ids": ["1893726451023847424"],
    "message_id": "1893726451029384192",
    "success": true
  },
  "field_boundary": {
    "tweet_media_field": "media",
    "tweet_media_value": "mediaUrl",
    "dm_media_field": "media_ids[0]",
    "dm_media_value": "mediaId",
    "forbidden_tweet_field": "media_ids"
  },
  "audit_row": {
    "record_type": "media_upload_handoff",
    "source_endpoint": "/api/v1/x/media",
    "account": "myxhandle",
    "media_id": "1893726451023847424",
    "media_url": "https://media.xquik.com/uploads/1893726451023847424.png",
    "tweet_id": "1895432178065391234",
    "message_id": "1893726451029384192",
    "handoff_format": "jsonl"
  },
  "handoff_state": "store_media_url_for_tweets_and_media_id_for_dms"
}
```

<CardGroup cols={2}>
  <Card title="Upload checkpoint" icon="paperclip">
    Store returned `mediaId`, `mediaUrl`, `success`, source URL or filename, and the upload endpoint.
  </Card>

  <Card title="Tweet URL checkpoint" icon="send">
    Store the public `mediaUrl` in `media`, plus returned `tweetId`, `chargedCredits`, and optional `writeActionId`.
  </Card>

  <Card title="Reply URL checkpoint" icon="message-square">
    Store `reply_to_tweet_id`, public `mediaUrl`, returned `tweetId`, and pending confirmation state when present.
  </Card>

  <Card title="DM ID checkpoint" icon="mail">
    Store exactly one uploaded `mediaId` in `media_ids`, plus returned `messageId` and recipient ID.
  </Card>

  <Card title="Field boundary" icon="split">
    Reject tweet handoffs that put uploaded media IDs in `media` or send `media_ids` to `POST /x/tweets`.
  </Card>

  <Card title="Audit row" icon="file-check">
    Store upload, tweet, reply, and DM IDs together so downstream systems can reconcile each media path.
  </Card>
</CardGroup>

## Step 1: Upload media by URL

Use JSON URL upload when an AI agent, MCP client, or workflow tool has a generated image or MP4 URL that Xquik should validate and host. For tweet-only workflows with already public image URLs or exactly 1 public MP4 video URL up to 100 MB, call `POST /x/tweets` directly with `media`.
The URL must use HTTPS, resolve to a public address, return a supported media content type, finish within 30 seconds, and stay under the 15,728,640-byte URL download cap.

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

```json theme={null}
{
  "mediaId": "1893726451023847424",
  "mediaUrl": "https://media.xquik.com/uploads/1893726451023847424.png",
  "success": true
}
```

### URL upload checklist

<CardGroup cols={2}>
  <Card title="HTTPS URL" icon="lock">
    Non-HTTPS URLs return `422 media_download_failed`.
  </Card>

  <Card title="Public host" icon="globe">
    Private or reserved IP targets are rejected.
  </Card>

  <Card title="Supported content type" icon="file-image">
    AVIF, GIF, JPEG, PNG, WebP, and MP4 are accepted.
  </Card>

  <Card title="Size cap" icon="gauge">
    Larger URL downloads than 15,728,640 bytes return `422 media_download_failed`.
  </Card>

  <Card title="30-second response window" icon="timer">
    Slow origins can time out before upload starts.
  </Card>
</CardGroup>

## Step 2: Post a tweet or reply with mediaUrl

`POST /x/tweets` accepts public media URLs in `media`. Send up to 4 images or exactly 1 MP4 video up to 100 MB. Use the `mediaUrl` returned by `POST /x/media`. Text-only tweet or reply writes cost 30 credits; attached media adds 2 credits per started MB across all files.

```bash theme={null}
curl -X POST https://xquik.com/api/v1/x/tweets \
  -H "x-api-key: xq_YOUR_KEY_HERE" \
  -H "Content-Type: application/json" \
  -d '{
    "account": "myxhandle",
    "text": "Product update with a new screenshot.",
    "media": ["https://media.xquik.com/uploads/1893726451023847424.png"]
  }' | jq
```

To post a media reply, send the same `media` URL array and add `reply_to_tweet_id`.

```bash theme={null}
curl -X POST https://xquik.com/api/v1/x/tweets \
  -H "x-api-key: xq_YOUR_KEY_HERE" \
  -H "Content-Type: application/json" \
  -d '{
    "account": "myxhandle",
    "text": "Here is the requested screenshot.",
    "reply_to_tweet_id": "1893704267862470862",
    "media": ["https://media.xquik.com/uploads/1893726451023847424.png"]
  }' | jq
```

```json theme={null}
{
  "tweetId": "1895432178065391234",
  "success": true
}
```

Store the action `id`, `request.hash`, `billing`, `result`, original `mediaUrl`, and parent `reply_to_tweet_id`. Poll [Get write action status](/api-reference/x-write/get-write-action-status) while `terminal` is `false`. Retry only when `safeToRetry` is `true`, using a new `Idempotency-Key`.

Do not send `media_ids` to `POST /x/tweets`. That endpoint rejects `media_ids` and expects the `media` URL array instead.

## Step 3: Send a DM with mediaId

`POST /x/dm/{userId}` accepts one uploaded media ID in `media_ids`. Use the `mediaId` returned by `POST /x/media`.

```bash theme={null}
curl -X POST https://xquik.com/api/v1/x/dm/44196397 \
  -H "x-api-key: xq_YOUR_KEY_HERE" \
  -H "Content-Type: application/json" \
  -d '{
    "account": "myxhandle",
    "text": "Here is the requested image.",
    "media_ids": ["1893726451023847424"]
  }' | jq
```

DMs accept exactly one uploaded media item. Send no `media_ids` field for text-only DMs.
Store `message_id`, `media_id`, recipient, account, and send status in shared handoff rows. Keep full DM body text in private systems only.

### JSON Lines handoff

For queues, CRM syncs, warehouse loads, or agent memory, write one record per upload and downstream write to `xquik-media-handoff.jsonl`.

#### Media upload row

```json theme={null}
{
  "record_type": "media_upload",
  "account": "myxhandle",
  "source": "https://example.com/image.png",
  "media_id": "1893726451023847424",
  "media_url": "https://media.xquik.com/uploads/1893726451023847424.png",
  "media_id_expires_in_hours": 24,
  "tweet_media_field": "media",
  "dm_media_field": "media_ids[0]",
  "handoff_format": "jsonl"
}
```

#### Tweet or reply row

```json theme={null}
{
  "record_type": "tweet_media_post",
  "account": "myxhandle",
  "tweet_id": "1895432178065391234",
  "reply_to_tweet_id": "1893704267862470862",
  "media_url": "https://media.xquik.com/uploads/1893726451023847424.png",
  "write_status": "posted",
  "handoff_format": "jsonl"
}
```

#### DM media row

```json theme={null}
{
  "record_type": "dm_media_send",
  "account": "myxhandle",
  "recipient_user_id": "44196397",
  "message_id": "1893726451029384192",
  "media_id": "1893726451023847424",
  "write_status": "sent",
  "handoff_format": "jsonl"
}
```

Use `media_url` for tweet and reply `media` arrays. Use `media_id` for the single DM `media_ids` item.

## Step 4: Upload a local file

Use multipart upload when your app has the file bytes. Supported formats are AVIF, GIF, JPEG, PNG, WebP, and MP4.

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

For MP4 files longer than 140 seconds, add `is_long_video=true`.

```bash theme={null}
curl -X POST https://xquik.com/api/v1/x/media \
  -H "x-api-key: xq_YOUR_KEY_HERE" \
  -F "account=myxhandle" \
  -F "file=@/path/to/video.mp4" \
  -F "is_long_video=true" | jq
```

## Cost and error handling

Upload media costs 10 credits per upload call. Posting a tweet or reply is a separate 30-credit create-tweet call before media surcharges; sending the DM is a separate 10-credit write call.

<CardGroup cols={2}>
  <Card title="Invalid input" icon="circle-alert">
    Status `400`. Error `invalid_input`. Check `account`, file type, file presence, URL presence, and `is_long_video`.
  </Card>

  <Card title="Billing or credits" icon="credit-card">
    Status `402`. Errors `no_subscription` or `insufficient_credits`. Subscribe or top up credits before retrying.
  </Card>

  <Card title="Reconnect account" icon="refresh-cw">
    Status `403`. Error `account_needs_reauth`. Reconnect the X account from the dashboard.
  </Card>

  <Card title="Media download failed" icon="file-warning">
    Status `422`. Error `media_download_failed`. Use an HTTPS URL that returns a supported media file, or switch to multipart upload.
  </Card>

  <Card title="Rate limit or temporary failure" icon="timer-reset">
    Status `429` or `503`. Retry with exponential backoff and respect `Retry-After` when present.
  </Card>
</CardGroup>

## Handoff checklist

<CardGroup cols={2}>
  <Card title="Tweet workflow" icon="send">
    Save `mediaUrl` and pass it to `POST /x/tweets` as `media`.
  </Card>

  <Card title="Reply workflow" icon="message-square">
    Save the parent tweet ID, `mediaUrl`, returned `tweetId`, and any `writeActionId`.
  </Card>

  <Card title="DM workflow" icon="mail">
    Save `mediaId` and pass it to `POST /x/dm/{userId}` as one `media_ids` item.
  </Card>

  <Card title="JSON Lines" icon="braces">
    Store upload, tweet/reply, or DM handoff rows in `xquik-media-handoff.jsonl` with `media_id` and `media_url`.
  </Card>

  <Card title="Agent workflow" icon="bot">
    Use JSON URL upload only when the agent needs Xquik to validate or host a
    generated URL, or needs a DM `mediaId`. For tweet-only public URLs, pass
    them directly to `POST /x/tweets`.
  </Card>

  <Card title="File workflow" icon="folder-open">
    Prefer multipart upload when your app owns the file bytes.
  </Card>
</CardGroup>

<Note>
  **Related:** [Upload Media](/api-reference/x-write/upload-media) · [Create Tweet](/api-reference/x-write/create-tweet) · [Send Direct Message](/api-reference/x-write/send-dm)
</Note>
