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

> Upload images, GIFs, WebP, AVIF, or MP4s, then use mediaUrl for tweets and mediaId for DMs with 1 attachment

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

Skip this endpoint when a tweet or reply already has public HTTPS image URLs or a public MP4 URL. Use `POST /x/media` only when Xquik must host a local file, validate a generated media URL, or return a `mediaId` for one-item DM `media_ids`. Tweets use the returned `mediaUrl`; DMs use the returned `mediaId`.

<CodeGroup>
  ```bash cURL 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
  ```

  ```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",
    },
    body: form,
  });
  if (!response.ok) {
    throw new Error(await response.text());
  }
  const result = await response.json();
  const handoff = {
    media_id: result.mediaId,
    media_url: result.mediaUrl,
    dm_media_ids: [result.mediaId],
    dm_endpoint: "/api/v1/x/dm/{userId}",
    tweet_media_url: result.mediaUrl,
    success: result.success,
  };
  ```

  ```python Python theme={null}
  import requests

  with open("/path/to/image.png", "rb") as media_file:
      response = requests.post(
          "https://xquik.com/api/v1/x/media",
          headers={"x-api-key": "xq_YOUR_KEY_HERE"},
          files={"file": media_file},
          data={"account": "myxhandle"},
      )
  response.raise_for_status()
  result = response.json()
  handoff = {
      "media_id": result["mediaId"],
      "media_url": result["mediaUrl"],
      "dm_media_ids": [result["mediaId"]],
      "dm_endpoint": "/api/v1/x/dm/{userId}",
      "tweet_media_url": result["mediaUrl"],
      "success": result["success"],
  }
  ```

  ```go Go theme={null}
  package main

  import (
      "bytes"
      "encoding/json"
      "fmt"
      "io"
      "mime/multipart"
      "net/http"
      "os"
  )

  type UploadMediaResponse struct {
      MediaID string `json:"mediaId"`
      MediaURL string `json:"mediaUrl"`
      Success bool `json:"success"`
  }

  func main() {
      var buf bytes.Buffer
      writer := multipart.NewWriter(&buf)

      _ = writer.WriteField("account", "myxhandle")

      file, _ := os.Open("/path/to/image.png")
      defer file.Close()
      part, _ := writer.CreateFormFile("file", "image.png")
      io.Copy(part, file)
      writer.Close()

      req, err := http.NewRequest("POST", "https://xquik.com/api/v1/x/media", &buf)
      if err != nil {
          panic(err)
      }
      req.Header.Set("x-api-key", "xq_YOUR_KEY_HERE")
      req.Header.Set("Content-Type", writer.FormDataContentType())

      resp, err := http.DefaultClient.Do(req)
      if err != nil {
          panic(err)
      }
      defer resp.Body.Close()
      if resp.StatusCode >= 300 {
          body, _ := io.ReadAll(resp.Body)
          panic(string(body))
      }

      var upload UploadMediaResponse
      if err := json.NewDecoder(resp.Body).Decode(&upload); err != nil {
          panic(err)
      }
      handoff := map[string]any{
          "media_id": upload.MediaID,
          "media_url": upload.MediaURL,
          "dm_media_ids": []string{upload.MediaID},
          "dm_endpoint": "/api/v1/x/dm/{userId}",
          "tweet_media_url": upload.MediaURL,
          "success": upload.Success,
      }
      encoded, err := json.Marshal(handoff)
      if err != nil {
          panic(err)
      }
      fmt.Println(string(encoded))
  }
  ```
</CodeGroup>

**URL-based upload (for AI agents and MCP clients):**

<CodeGroup>
  ```bash cURL 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
  ```

  ```javascript Node.js theme={null}
  const response = await fetch("https://xquik.com/api/v1/x/media", {
    method: "POST",
    headers: {
      "x-api-key": "xq_YOUR_KEY_HERE",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      account: "myxhandle",
      url: "https://example.com/image.png",
    }),
  });
  if (!response.ok) {
    throw new Error(await response.text());
  }
  const result = await response.json();
  const handoff = {
    media_id: result.mediaId,
    media_url: result.mediaUrl,
    dm_media_ids: [result.mediaId],
    dm_endpoint: "/api/v1/x/dm/{userId}",
    tweet_media_url: result.mediaUrl,
    success: result.success,
  };
  ```

  ```python Python theme={null}
  import requests

  response = requests.post(
      "https://xquik.com/api/v1/x/media",
      headers={
          "x-api-key": "xq_YOUR_KEY_HERE",
          "Content-Type": "application/json",
      },
      json={
          "account": "myxhandle",
          "url": "https://example.com/image.png",
      },
  )
  response.raise_for_status()
  result = response.json()
  handoff = {
      "media_id": result["mediaId"],
      "media_url": result["mediaUrl"],
      "dm_media_ids": [result["mediaId"]],
      "dm_endpoint": "/api/v1/x/dm/{userId}",
      "tweet_media_url": result["mediaUrl"],
      "success": result["success"],
  }
  ```
</CodeGroup>

The file and URL examples both build the same handoff shape. Store `media_id`, `media_url`, `dm_media_ids`, `dm_endpoint`, and `tweet_media_url`; pass `dm_media_ids` only to `POST /x/dm/{userId}`, and pass `tweet_media_url` in the `media` array on `POST /x/tweets`.

## Media upload handoff

Use `POST /x/media` when an app, support queue, CRM, workflow tool, or AI agent needs to turn a local file or hosted HTTPS media URL into media that Xquik can attach to a tweet, reply, or DM.

<CardGroup cols={2}>
  <Card title="mediaUrl" icon="image">
    Store `mediaUrl` for tweet and reply attachments. Pass it in the `media`
    array on `POST /x/tweets`.
  </Card>

  <Card title="mediaId" icon="paperclip">
    Store `mediaId` for direct message attachments. Pass it as the only item in
    `media_ids` on `POST /x/dm/{userId}`.
  </Card>

  <Card title="success" icon="circle-check">
    Store `success` to confirm the upload completed before the next write call.
  </Card>

  <Card title="account" icon="user">
    Store the `account` you submitted to audit which connected X account
    uploaded and sent the media.
  </Card>

  <Card title="Source input" icon="file-image">
    Store the source URL or filename to reconcile generated images, support
    files, or CRM assets with the uploaded media.
  </Card>
</CardGroup>

For tweets with already-public image URLs or exactly 1 public MP4 video URL up to 100 MB, skip this endpoint and call [`POST /x/tweets`](/api-reference/x-write/create-tweet) directly with `media`. After uploading through this endpoint, call `POST /x/tweets` and pass `media: ["<mediaUrl>"]`. To post a media reply, also pass `reply_to_tweet_id`. Do not send `media_ids` to `POST /x/tweets`; that endpoint returns `400 unsupported_field` and expects public media URLs.

For DMs, call [`POST /x/dm/{userId}`](/api-reference/x-write/send-dm) after upload and pass `media_ids: ["<mediaId>"]`. DMs accept exactly 1 uploaded media ID.

For JSON URL upload, the URL must use HTTPS, resolve to a public address, return AVIF, GIF, JPEG, PNG, WebP, or MP4 content, finish within 30 seconds, and stay at or below 15,728,640 bytes. URL download failures return `422 media_download_failed`; fix the URL or switch to multipart upload.

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.

## Headers

<ParamField header="x-api-key" type="string" required>
  Your API key. Session cookie authentication is also supported. Generate a key from the [dashboard](https://xquik.com/dashboard).
</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>
  The connected X account to upload media as. Must be a username you have connected to your Xquik account.
</ParamField>

<ParamField body="file" type="binary">
  The media file to upload. Required if `url` is not provided. Supported formats: AVIF, GIF, JPEG, PNG, WebP, MP4.
</ParamField>

<ParamField body="url" type="string">
  HTTPS URL to download media from. Required if `file` is not provided. The server fetches the file from this URL. Useful for AI agents and MCP clients that work with URLs instead of binary file uploads.
</ParamField>

<ParamField body="is_long_video" type="boolean">
  Multipart video uploads only. Set to `true` for `video/mp4` files longer than 140 seconds. Defaults to `false`.
</ParamField>

## Response

<Tabs>
  <Tab title="200 OK">
    <ResponseField name="mediaId" type="string">The uploaded media ID. Supply it as the only item in the `media_ids` array on `POST /x/dm/{userId}` when sending a direct message with media.</ResponseField>
    <ResponseField name="mediaUrl" type="string">A public media URL. Pass it in the `media` array on `POST /x/tweets` when creating a tweet with uploaded media.</ResponseField>
    <ResponseField name="success" type="boolean">Always `true` on success.</ResponseField>

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

  <Tab title="400 Invalid Input">
    ```json theme={null}
    { "error": "invalid_input", "message": "Unsupported file format or missing required fields" }
    ```

    Missing `account`, or neither `file` nor `url` provided, or the media is not a supported format (AVIF, GIF, JPEG, PNG, WebP, MP4).
  </Tab>

  <Tab title="401 Unauthenticated">
    ```json theme={null}
    { "error": "unauthenticated", "message": "Missing or invalid API key" }
    ```

    Missing or invalid API key.
  </Tab>

  <Tab title="402 Subscription Required">
    ```json theme={null}
    { "error": "no_subscription", "message": "No active subscription" }
    ```

    An active subscription is required to upload media. Subscribe from the [dashboard](https://xquik.com/subscription).
  </Tab>

  <Tab title="402 Insufficient Credits">
    ```json theme={null}
    { "error": "insufficient_credits", "message": "Insufficient credits" }
    ```

    Insufficient credits for this operation. Top up your credit balance from the [dashboard](https://xquik.com/dashboard).
  </Tab>

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

    The specified `account` is not connected to your Xquik account. Connect it from the [dashboard](https://xquik.com/dashboard).
  </Tab>

  <Tab title="403 Account Needs Reauth">
    ```json theme={null}
    { "error": "account_needs_reauth" }
    ```

    The connected X account failed a recent login attempt and needs to be reconnected. Reconnect it from the [dashboard](https://xquik.com/dashboard).
  </Tab>

  <Tab title="403 Account Restricted">
    ```json theme={null}
    { "error": "account_restricted" }
    ```

    The connected X account is locked, suspended, recovering, or temporarily restricted. Resolve the restriction on X before retrying.
  </Tab>

  <Tab title="422 Write Rejected">
    ```json theme={null}
    { "error": "x_content_too_long", "message": "..." }
    ```

    X rejected the write or the media URL could not be downloaded. Possible codes: `x_content_too_long`, `x_duplicate_action`, `x_account_suspended`, `x_account_protected`, `x_target_not_found`, `x_account_feature_required`, `x_rejected`, `media_download_failed` (URL fetch path only - body.url not HTTPS, resolves to private IP, file too large, origin error, or timeout). See [error handling](/guides/error-handling) for details.
  </Tab>

  <Tab title="429 Rate Limited">
    ```json theme={null}
    { "error": "rate_limit_exceeded", "retryAfter": 60 }
    ```

    The request hit an Xquik tier limit, or X throttled the upload. Possible codes: `rate_limit_exceeded`, `x_rate_limited`, or `x_daily_limit`. Respect the `Retry-After` header when present.
  </Tab>

  <Tab title="503 Transient Error">
    ```json theme={null}
    { "error": "x_transient_error", "message": "..." }
    ```

    A transient write service issue occurred. Safe to retry with exponential backoff.
  </Tab>

  <Tab title="500 Upload Failed">
    ```json theme={null}
    { "error": "x_write_failed", "message": "Write action failed unexpectedly. Contact support if this persists." }
    ```

    The upload could not be completed. Retry after a short delay, then contact support if the failure persists.
  </Tab>
</Tabs>

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

<Tip>
  Use `mediaUrl` when [creating tweets](/api-reference/x-write/create-tweet). Use `mediaId` as the only item in the `media_ids` array when [sending a direct message](/api-reference/x-write/send-dm).
</Tip>

<Note>
  **Related endpoints:** [Create Tweet](/api-reference/x-write/create-tweet) to post with the returned `mediaUrl`, or [Send Direct Message](/api-reference/x-write/send-dm) to send a DM with one uploaded media item.
</Note>
