> ## 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 downloader API for photos & video

> Download images, videos, and animated GIFs from 1-50 tweets. Returns one gallery URL plus cache or bulk counts. Fresh tweets cost 1 credit. Cache hits are free.

<Panel>
  <Tabs defaultTabIndex={0} sync={false}>
    <Tab title="200" id="response-x-download-media-200">
      ```json theme={null}
      {
        "tweetId": "1234567890",
        "galleryUrl": "https://xquik.com/gallery/abc123",
        "cacheHit": false
      }
      ```
    </Tab>

    <Tab title="400" id="response-x-download-media-400">
      ```json theme={null}
      {
        "error": "invalid_input",
        "message": "Invalid input. Check the request body."
      }
      ```
    </Tab>

    <Tab title="401" id="response-x-download-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-download-media-402">
      ```json theme={null}
      {
        "error": "insufficient_credits",
        "message": "Insufficient credits. Top up or subscribe to continue."
      }
      ```
    </Tab>

    <Tab title="404" id="response-x-download-media-404">
      ```json theme={null}
      {
        "error": "tweet_not_found",
        "message": "Tweet not found."
      }
      ```
    </Tab>

    <Tab title="424" id="response-x-download-media-424">
      ```json theme={null}
      {
        "error": "x_api_unavailable",
        "message": "X data source temporarily unavailable. Try again later."
      }
      ```
    </Tab>

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

    <Tab title="502" id="response-x-download-media-502">
      ```json theme={null}
      {
        "error": "x_api_unavailable",
        "message": "X data source temporarily unavailable. Try again later."
      }
      ```
    </Tab>
  </Tabs>
</Panel>

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

Send 1-50 tweet IDs or URLs.
The route downloads media from one tweet or a batch of 50.
Each response contains a gallery of photos, videos, and GIFs.

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

This endpoint creates a saved media gallery from 1-50 tweet URLs or IDs.
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. You can also authenticate with an OAuth bearer token.
</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`. Use it when `tweetInput` is absent.
</ParamField>

<ParamField body="tweetUrl" type="string">
  Tweet URL alias for `tweetInput`. Use it when both earlier fields are absent.
</ParamField>

<ParamField body="tweetIds" type="string[]">
  Array of tweet URLs or IDs for bulk download. Maximum 50 string items. The route skips invalid IDs. It 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. Later jobs can then store the gallery
link. It is not 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 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 the route skips invalid or failed IDs.
  </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>

## Store a tweet media download manifest

Keep one manifest row per single or bulk request. A gallery URL represents the
download result. A gallery URL is neither an uploaded media ID nor a direct asset URL.

| Manifest column | Response or request source | Reconciliation rule |
| - | - | - |
| `input_mode` | Presence of non-empty `tweetIds` | Store `single` or `bulk`. |
| `requested_tweet_ids` | Normalized request inputs | Keep every valid ID submitted to the download route. |
| `tweet_id` | Single response `tweetId` | Store the resolved Tweet ID for single mode. |
| `gallery_url` | Response `galleryUrl` | Use as the shareable downloaded-media result. |
| `cache_hit` | Single response `cacheHit` | Record whether the single download used free cached media. |
| `successful_tweet_count` | Bulk response `totalTweets` | Count only successful tweets that contained media. |
| `media_item_count` | Bulk response `totalMedia` | Reconcile the number of downloaded images, videos, and GIFs. |
| `requested_at` | Integration timestamp | Audit when the download request started. |
| `completed_at` | Integration timestamp | Record when your integration stores the gallery. |

Use these outcomes when reconciling a download request:

* `cacheHit: true` confirms a free cached single result.
* `cacheHit: false` confirms a fresh single result.
* If `totalTweets` is below the valid input count, review skipped Tweet IDs.
* A `400 no_media` response means the source tweet has no downloadable media.
* Keep rejected inputs while processing valid Tweet IDs.
* Split more than 50 inputs into groups of 50 or fewer.

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

### 200 OK (single)

<ResponseField name="tweetId" type="string">
  Identifies the resolved tweet.
</ResponseField>

<ResponseField name="galleryUrl" type="string">
  Links the shareable gallery containing all downloaded media.
</ResponseField>

<ResponseField name="cacheHit" type="boolean">
  Shows whether the cache supplied the media. Cached requests consume no credits.
</ResponseField>

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

### 200 OK (bulk)

<ResponseField name="galleryUrl" type="string">
  Links the combined gallery containing media from all tweets.
</ResponseField>

<ResponseField name="totalTweets" type="number">
  Counts processed tweets with media.
</ResponseField>

<ResponseField name="totalMedia" type="number">
  Counts downloaded images, videos, and GIFs.
</ResponseField>

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

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

### 400 Invalid tweet ID

```json theme={null}
{ "error": "invalid_tweet_id", "message": "Tweet ID is empty or invalid" }
```

Xquik could not resolve the provided tweet ID or URL to a valid tweet ID.

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

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

### 401 Unauthenticated

```json theme={null}
{ "error": "unauthenticated", "message": "Missing or invalid API key" }
```

Supply a valid API key or OAuth bearer token.

### 402 Insufficient credits

```json theme={null}
{
  "error": "insufficient_credits",
  "message": "Insufficient credits. Top up or subscribe to continue."
}
```

The available balance cannot cover the download. Top up or subscribe from the [dashboard billing page](https://dashboard.xquik.com/en/account?tab=subscription).

### 404 Tweet not found

```json theme={null}
{ "error": "tweet_not_found", "message": "Tweet not found" }
```

The tweet ID is valid, but Xquik cannot fetch the tweet or it no longer exists.

### 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 fast. Respect the `Retry-After` header before retrying.

### 502 X API unavailable

```json theme={null}
{ "error": "x_api_unavailable" }
```

The read service failed. Retry after a short delay.

### 424 Dependency failed

```json theme={null}
{ "error": "x_api_unavailable" }
```

The opt-in normalized contract returns 424 when the read service fails.
Send `xquik-api-contract: 2026-04-29` to opt in. Default v1 returns 502.

## Twitter media downloader questions

### How do I download Twitter media?

Copy the tweet link or numeric ID. Send it through `tweetInput`. The gallery can
contain Twitter videos and GIFs plus images.

### Can I save Twitter videos or GIFs on a phone?

Yes. Open the returned `galleryUrl` in a web browser. The API does not return
direct per-file URLs.

### Does this work like a browser extension?

No. This is a server-side API. Keep the API key or bearer token server-side.

### Can I download Twitter video files directly?

No. This route creates a gallery. It does not expose
per-file downloads or resolution controls.

### Can I download media from many tweets?

Yes. Send 1-50 tweet IDs through `tweetIds`.
The response returns one gallery with tweet and media counts.

<Info>
  Xquik meters the first download against your monthly credit allowance. Later requests for the same tweet return cached URLs at no cost (`cacheHit: true`). Xquik saves all downloads to your gallery at `https://xquik.com/gallery`.
</Info>

<Note>
  This endpoint accepts an API key or OAuth bearer token. Keep either credential server-side.

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

<div className="related-api-links">
  <Accordion title="Related tweet, reply & media APIs" icon="link">
    * Tweets: [Get tweet](/api-reference/x/get-tweet) · [Batch tweets](/api-reference/x/batch-tweets) · [Tweet thread](/api-reference/x/tweet-thread) · [Hidden replies](/api-reference/x/tweet-hidden-replies) · [Translate tweet](/api-reference/x/tweet-translation) · [Embed tweet](/api-reference/x/tweet-embed) · [Resolve links](/api-reference/x/resolve-links) · [Tweet subtitles](/api-reference/x/tweet-subtitles) · [X Article](/api-reference/x/get-article)
    * Analysis: [Sentiment analysis](/api-reference/x/sentiment-analysis) · [Brand mentions](/api-reference/x/brand-monitoring) · [News classification](/api-reference/x/news-classification) · [Market signals](/api-reference/x/market-signals) · [Viral score](/api-reference/x/viral-score) · [Classify posts](/api-reference/x/classify-tweets)
    * Engagement: [Tweet replies](/api-reference/x/tweet-replies) · [Quote tweets](/api-reference/x/tweet-quotes) · [Likers](/api-reference/x/favoriters) · [Reposters](/api-reference/x/retweeters) · [Check repost](/api-reference/x/tweet-repost-check)
    * Profiles: [User tweets](/api-reference/x/user-tweets) · [Batch user tweets](/api-reference/x/batch-user-tweets) · [User replies](/api-reference/x/user-replies) · [User likes](/api-reference/x/user-likes) · [User media](/api-reference/x/user-media) · [User highlights](/api-reference/x/user-highlights) · [User articles](/api-reference/x/user-articles)
    * Feeds: [List tweets](/api-reference/x/list-tweets) · [Trends](/api-reference/x/trends) · [Trend locations](/api-reference/x/trend-locations) · [Search Spaces](/api-reference/x/search-spaces) · [Get Space](/api-reference/x/get-space) · [Space replay](/api-reference/x/space-replay) · [Get broadcast](/api-reference/x/get-broadcast) · [Hashflags](/api-reference/x/hashflags) · [Search places](/api-reference/x/search-places) · [Download media](/api-reference/x/download-media)
  </Accordion>
</div>

<div className="related-api-links">
  <Accordion title="Related timeline, bookmark & notification APIs" icon="link">
    * Account feeds: [Home timeline](/api-reference/x/timeline) · [Notifications](/api-reference/x/notifications) · [Mentions](/api-reference/x/user-mentions)
    * Saved and private: [Bookmarks](/api-reference/x/bookmarks) · [Bookmark folders](/api-reference/x/bookmark-folders) · [DM history](/api-reference/x/dm-history)
  </Accordion>
</div>


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