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

# Download media

> Download images, videos, and GIFs from tweets. Single or bulk (up to 50). Returns a gallery URL

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

<Callout icon="coins" color="#5c3327">
  **1 credit per fresh tweet processed with media** · cache hits are free · [All plans](https://xquik.com/#pricing) from \$0.00012/credit
</Callout>

Use this endpoint to turn one tweet, or up to 50 tweet URLs or IDs, into a
saved media gallery. The response gives a `galleryUrl` plus cache or bulk
counts; it does not return per-file downloads, file metadata, or an uploaded
`mediaId`.

<CodeGroup>
  ```bash cURL (single) theme={null}
  curl -X POST https://xquik.com/api/v1/x/media/download \
    -H "x-api-key: xq_YOUR_KEY_HERE" \
    -H "Content-Type: application/json" \
    -d '{
      "tweetInput": "1893456789012345678"
    }' | jq
  ```

  ```bash cURL (bulk) theme={null}
  curl -X POST https://xquik.com/api/v1/x/media/download \
    -H "x-api-key: xq_YOUR_KEY_HERE" \
    -H "Content-Type: application/json" \
    -d '{
      "tweetIds": ["1893456789012345678", "1893456789012345999", "1893456789012346000"]
    }' | jq
  ```

  ```javascript Node.js theme={null}
  const singleTweetId = "1893456789012345678";

  // Single tweet
  const single = await fetch("https://xquik.com/api/v1/x/media/download", {
    method: "POST",
    headers: {
      "x-api-key": "xq_YOUR_KEY_HERE",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ tweetInput: singleTweetId }),
  });
  const singleResult = await single.json();
  const singleRow = {
    input_mode: "single",
    requested_tweet_id: singleTweetId,
    tweet_id: singleResult.tweetId,
    gallery_url: singleResult.galleryUrl,
    cache_hit: singleResult.cacheHit,
  };
  process.stdout.write(`${JSON.stringify(singleRow)}\n`);

  const bulkTweetIds = ["1893456789012345678", "1893456789012345999"];

  // Bulk (up to 50 tweets)
  const bulk = await fetch("https://xquik.com/api/v1/x/media/download", {
    method: "POST",
    headers: {
      "x-api-key": "xq_YOUR_KEY_HERE",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ tweetIds: bulkTweetIds }),
  });
  const bulkResult = await bulk.json();
  const bulkRow = {
    input_mode: "bulk",
    requested_tweet_ids: bulkTweetIds,
    gallery_url: bulkResult.galleryUrl,
    successful_tweet_count: bulkResult.totalTweets,
    media_item_count: bulkResult.totalMedia,
  };
  process.stdout.write(`${JSON.stringify(bulkRow)}\n`);
  ```

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

  single_tweet_id = "1893456789012345678"

  # Single tweet
  single = requests.post(
      "https://xquik.com/api/v1/x/media/download",
      headers={"x-api-key": "xq_YOUR_KEY_HERE"},
      json={"tweetInput": single_tweet_id},
  )
  single_result = single.json()
  single_row = {
      "input_mode": "single",
      "requested_tweet_id": single_tweet_id,
      "tweet_id": single_result["tweetId"],
      "gallery_url": single_result["galleryUrl"],
      "cache_hit": single_result["cacheHit"],
  }
  print(json.dumps(single_row))

  bulk_tweet_ids = ["1893456789012345678", "1893456789012345999"]

  # Bulk (up to 50 tweets)
  bulk = requests.post(
      "https://xquik.com/api/v1/x/media/download",
      headers={"x-api-key": "xq_YOUR_KEY_HERE"},
      json={"tweetIds": bulk_tweet_ids},
  )
  bulk_result = bulk.json()
  bulk_row = {
      "input_mode": "bulk",
      "requested_tweet_ids": bulk_tweet_ids,
      "gallery_url": bulk_result["galleryUrl"],
      "successful_tweet_count": bulk_result["totalTweets"],
      "media_item_count": bulk_result["totalMedia"],
  }
  print(json.dumps(bulk_row))
  ```

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

  import (
      "bytes"
      "encoding/json"
      "fmt"
      "log"
      "net/http"
  )

  type MediaDownloadResponse struct {
      CacheHit    bool   `json:"cacheHit"`
      GalleryURL  string `json:"galleryUrl"`
      TweetID     string `json:"tweetId"`
      TotalMedia  int    `json:"totalMedia"`
      TotalTweets int    `json:"totalTweets"`
  }

  type MediaDownloadRow struct {
      InputMode        string `json:"input_mode"`
      RequestedTweetID string `json:"requested_tweet_id"`
      TweetID          string `json:"tweet_id"`
      GalleryURL       string `json:"gallery_url"`
      CacheHit         bool   `json:"cache_hit"`
  }

  func main() {
      // Single tweet
      tweetID := "1893456789012345678"
      payload, _ := json.Marshal(map[string]interface{}{
          "tweetInput": tweetID,
      })

      req, err := http.NewRequest("POST", "https://xquik.com/api/v1/x/media/download", bytes.NewReader(payload))
      if err != nil {
          log.Fatal(err)
      }
      req.Header.Set("x-api-key", "xq_YOUR_KEY_HERE")
      req.Header.Set("Content-Type", "application/json")

      resp, err := http.DefaultClient.Do(req)
      if err != nil {
          log.Fatal(err)
      }
      defer resp.Body.Close()

      var result MediaDownloadResponse
      if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
          log.Fatal(err)
      }

      row := MediaDownloadRow{
          InputMode:        "single",
          RequestedTweetID: tweetID,
          TweetID:          result.TweetID,
          GalleryURL:       result.GalleryURL,
          CacheHit:         result.CacheHit,
      }
      output, err := json.Marshal(row)
      if err != nil {
          log.Fatal(err)
      }
      fmt.Println(string(output))
  }
  ```
</CodeGroup>

## Headers

<ParamField header="x-api-key" type="string" required>
  Your API key. Session cookie authentication is also supported.
</ParamField>

<ParamField header="Content-Type" type="string" required>
  Must be `application/json`.
</ParamField>

## Body

Use `tweetIds` for bulk downloads. If `tweetIds` is present with at least 1 string value, the route uses bulk mode and ignores single-tweet fields. Otherwise, use `tweetInput`, `tweetId`, or `tweetUrl` for a single tweet.

<ParamField body="tweetInput" type="string">
  Tweet URL or numeric tweet ID for a single download. Accepts `x.com` and `twitter.com` URL formats.
</ParamField>

<ParamField body="tweetId" type="string">
  Numeric tweet ID alias for `tweetInput`. Used when `tweetInput` is not provided.
</ParamField>

<ParamField body="tweetUrl" type="string">
  Tweet URL alias for `tweetInput`. Used when `tweetInput` and `tweetId` are not provided.
</ParamField>

<ParamField body="tweetIds" type="string[]">
  Array of tweet URLs or IDs for bulk download. Maximum 50 string items. Invalid IDs are skipped; the request fails only when no valid tweet IDs remain.
</ParamField>

## Media download handoff

Use this endpoint when your agent needs a saved gallery for tweet images, videos, or GIFs.
Write one manifest row per request so downstream jobs can store the gallery
link without treating it as an uploaded media ID or an individual media file
URL.

<CardGroup cols={2}>
  <Card title="Gallery URL" icon="images">
    Store `gallery_url` from `galleryUrl` as the durable link for downloaded media.
  </Card>

  <Card title="Single tweet" icon="message-square">
    Store `requested_tweet_id`, `tweet_id`, and `cache_hit`. `cacheHit: true` means the single-tweet request used cached media and is free.
  </Card>

  <Card title="Bulk result" icon="list-checks">
    Store `requested_tweet_ids`, `successful_tweet_count` from `totalTweets`, and `media_item_count` from `totalMedia`. `totalTweets` counts successful tweets with media after invalid or failed IDs are skipped.
  </Card>

  <Card title="Input mode" icon="list-filter">
    Send `tweetIds` for bulk. When it contains at least 1 string, bulk mode ignores `tweetInput`, `tweetId`, and `tweetUrl`.
  </Card>

  <Card title="Batch limit" icon="list-ordered">
    Keep `tweetIds` at 50 items or fewer. Split larger backfills into multiple requests.
  </Card>

  <Card title="Write handoff" icon="send">
    This endpoint creates a gallery download, not an uploaded media ID. Use [Upload Media](/api-reference/x-write/upload-media) before DMs or hosted tweet assets.
  </Card>
</CardGroup>

Fresh downloads cost 1 credit per tweet processed with media. Cached single downloads return `cacheHit: true` and are free. Bulk responses do not return `freshCount`; store the request IDs with `totalTweets` and `totalMedia` for reconciliation.

## Response

<Tabs>
  <Tab title="200 OK (single)">
    <ResponseField name="tweetId" type="string">
      Resolved tweet ID.
    </ResponseField>

    <ResponseField name="galleryUrl" type="string">
      Shareable gallery page URL with all downloaded media.
    </ResponseField>

    <ResponseField name="cacheHit" type="boolean">
      `true` if media was served from cache (no usage consumed).
    </ResponseField>

    ```json theme={null}
    {
      "tweetId": "1893456789012345678",
      "galleryUrl": "https://xquik.com/gallery/abc123",
      "cacheHit": false
    }
    ```
  </Tab>

  <Tab title="200 OK (bulk)">
    <ResponseField name="galleryUrl" type="string">
      Combined gallery page URL containing media from all tweets.
    </ResponseField>

    <ResponseField name="totalTweets" type="number">
      Number of tweets processed.
    </ResponseField>

    <ResponseField name="totalMedia" type="number">
      Total media items downloaded across all tweets.
    </ResponseField>

    ```json theme={null}
    {
      "galleryUrl": "https://xquik.com/gallery/def456",
      "totalTweets": 3,
      "totalMedia": 7
    }
    ```
  </Tab>

  <Tab title="400 Invalid Input">
    ```json theme={null}
    { "error": "invalid_input", "message": "Invalid request body" }
    ```

    Missing, malformed, or non-JSON request body. Malformed JSON can also return `invalid_json`.
  </Tab>

  <Tab title="400 Invalid Tweet ID">
    ```json theme={null}
    { "error": "invalid_tweet_id", "message": "Tweet ID is empty or invalid" }
    ```

    The provided tweet ID or URL could not be resolved to a valid tweet ID.
  </Tab>

  <Tab title="400 Too Many Tweets">
    ```json theme={null}
    { "error": "too_many_tweets", "message": "Max 50 tweets per request" }
    ```

    The `tweetIds` array exceeds the 50-item limit. Split into multiple requests.
  </Tab>

  <Tab title="400 No Media">
    ```json theme={null}
    { "error": "no_media", "message": "Tweet has no downloadable media" }
    ```

    The tweet does not contain any images, videos, or GIFs.
  </Tab>

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

    Missing or invalid API key / session cookie.
  </Tab>

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

    An active subscription or enough credits are required. Subscribe or top up from the [dashboard](https://xquik.com/subscription).
  </Tab>

  <Tab title="404 Tweet Not Found">
    ```json theme={null}
    { "error": "tweet_not_found", "message": "Tweet not found" }
    ```

    The tweet ID is valid, but the tweet cannot be fetched or no longer exists.
  </Tab>

  <Tab title="429 Rate Limit Exceeded">
    ```json theme={null}
    {
      "error": "rate_limit_exceeded",
      "message": "Too many requests. Try again later.",
      "retryAfter": 60
    }
    ```

    The API key, user, or plan tier is sending requests too quickly. Respect the `Retry-After` header before retrying.
  </Tab>

  <Tab title="502 X API unavailable">
    ```json theme={null}
    { "error": "x_api_unavailable" }
    ```

    The read service returned an error. Retry after a short delay.
  </Tab>

  <Tab title="424 Dependency Failed">
    ```json theme={null}
    { "error": "x_api_unavailable" }
    ```

    Returned when you opt into the normalized v1 response contract and the read service is unavailable.
  </Tab>
</Tabs>

<Info>
  First download is metered and counts toward your monthly credit allowance. Subsequent requests for the same tweet return cached URLs at no cost (`cacheHit: true`). All downloads are saved to your gallery at `https://xquik.com/gallery`.
</Info>

<Note>
  This endpoint supports **dual authentication**: API key (`x-api-key` header) or session cookie from the dashboard.

  **Related:** [Get Tweet](/api-reference/x/get-tweet) to look up tweet details and metrics, or use the `xquik` [MCP tool](/mcp/tools#xquik) for AI agent access.
</Note>
