# Twitter (X) Media Downloader (`dataspryva/twitter-x-media-downloader`) Actor

Download videos, photos, GIFs, audio and poster images from public Twitter/X posts - no login, no cookies. Works with post links or usernames, supports every video quality, audio-only M4A/MP3, quoted-post media and public signed download links.

- **URL**: https://apify.com/dataspryva/twitter-x-media-downloader.md
- **Developed by:** [Dataspryva](https://apify.com/dataspryva) (community)
- **Categories:** Social media, Videos, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.49 / 1,000 results

This Actor is paid per event and usage. You are charged both the fixed price for specific events and for Apify platform usage.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Twitter (X) Media Downloader

Collect public Twitter / X media without an account, cookies or a browser. Paste post links or ask for a profile's media posts and get the actual files back — videos in any available quality, photos, GIFs, audio-only tracks and video posters — each with a public download link.

### What it does

- **Post mode** — one link or ID per post, including `x.com`, `twitter.com`, `mobile.twitter.com`, `/i/web/status/` links, `/status/<id>/video/1` links and bare numeric IDs.
- **Profile mode** — walks a profile's media posts page by page, not just the handful shown to logged-out visitors.
- **Both modes in one run** — results are de-duplicated across the two paths.
- **Status rows** — deleted, protected, media-less and rate-limited targets produce an honest status row instead of a silent empty run.

### Services

| Service | What you get |
|---|---|
| Video download | Best, 2160p, 1080p, 720p, 480p or smallest MP4; every quality can be saved as separate files |
| Photo download | Original, large, medium or small image files |
| GIF download | The MP4 file X serves for animated GIFs |
| Audio extraction | M4A (AAC) or MP3 soundtrack of a video |
| Poster images | The video/GIF thumbnail as a separate photo file |
| Quoted-post media | Media inside a quoted post, marked `mediaSource: "quoted post"` |
| Author and post context | Text, date, engagement counts, author name/avatar/verification/followers, hashtags, mentions, links, reply/quote flags and quoted post IDs |
| Profile context | Profile ID, follower count and media count on rows found through a username |
| Profile filters | Minimum likes, minimum reposts and verified-authors-only |

### How it works

The actor reads X's public post data and the FxTwitter mirror, combines the fields each source is best at, and falls back to the X status page and oEmbed when a post looks missing. Videos and GIFs use X's direct CDN; if a direct file cannot be fetched, yt-dlp is tried as a last resort. Files are streamed to disk, checked for a sane size, stored in the run's key-value store and linked with a signed public URL. No request uses your cookies, login or API key.

### Input

| Field | Type | Default | Description |
|---|---|---|---|
| `postUrls` | array | – | Post links or bare post IDs (the prefill uses one NASA post) |
| `usernames` | array | – | Handles, `@handles` or profile links to scan |
| `maxPostsPerUser` | integer | `20` | Media posts to check per profile (1–1000) |
| `maxItems` | integer | `100` | Hard cap on media rows for the whole run (1–10000) |
| `mediaTypes` | array | `["video", "photo", "gif"]` | `video`, `photo`, `gif` |
| `imageQuality` | string | `orig` | `orig`, `large`, `medium`, `small` |
| `videoQuality` | string | `best` | `best`, `2160p`, `1080p`, `720p`, `480p`, `worst` |
| `downloadAllQualities` | boolean | `false` | One row and file per available MP4 quality |
| `includePosterImage` | boolean | `true` | Save each video/GIF poster as a photo |
| `includeAltText` | boolean | `true` | Attach X's image description when present |
| `audioOnly` | boolean | `false` | Save only the audio track of videos; photos and GIFs are skipped |
| `audioFormat` | string | `m4a` | `m4a` or `mp3` when audio-only is on |
| `includeQuotedMedia` | boolean | `true` | Include media from a quoted post |
| `includeMetadata` | boolean | `true` | Attach post, author and engagement context |
| `minLikes` | integer | `0` | Profile scans only: skip posts below this like count |
| `minRetweets` | integer | `0` | Profile scans only: skip posts below this repost count |
| `onlyVerifiedAuthors` | boolean | `false` | Profile scans only: skip unverified authors |
| `saveToFile` | boolean | `true` | Store files and return signed links; off means metadata and CDN links only |

At least one post link/ID or username is required. Directly linked posts are always collected; the three filters apply while scanning usernames.

#### Example: download two posts

```json
{
  "postUrls": [
    "https://x.com/NASA/status/2107095768821576019",
    "https://x.com/SheCodeAfrica/status/2107516524965794217"
  ],
  "videoQuality": "1080p",
  "maxItems": 10
}
```

#### Example: scan a profile's media

```json
{
  "usernames": ["NASA"],
  "maxPostsPerUser": 50,
  "maxItems": 100,
  "mediaTypes": ["video", "photo"],
  "minLikes": 100,
  "onlyVerifiedAuthors": true
}
```

#### Example: audio-only MP3

```json
{
  "postUrls": ["https://x.com/NASA/status/2107095768821576019"],
  "audioOnly": true,
  "audioFormat": "mp3"
}
```

### Output

Every dataset row has `type`: `media file` for a collectible file, or `status` for a run message.

| Group | Fields |
|---|---|
| Identity | `tweetId`, `tweetUrl`, `postedAt`, `conversationId`, `inReplyToTweetId`, `isReply`, `isQuote`, `isRetweet`, `quotedTweetId`, `quotedTweetUrl` |
| Media | `mediaType`, `mediaUrl`, `mediaUrlOriginal`, `contentType`, `mediaId`, `mediaKey`, `thumbnailUrl`, `posterImageUrl`, `filename`, `fileSizeBytes`, `width`, `height`, `durationSeconds`, `focusRects`, `allowDownload` |
| Download | `downloadUrl`, `downloadStatus`, `message` |
| Video quality | `variantQuality`, `variantBitrate`, `availableQualities`, `qualityCount`, `isBestQuality` |
| Audio | `audioFormat` |
| Author | `uploaderUsername`, `uploaderName`, `uploaderVerified`, `uploaderAvatarUrl`, `uploaderFollowers` |
| Post context | `tweetText`, `likeCount`, `retweetCount`, `replyCount`, `quoteCount`, `bookmarkCount`, `viewCount`, `engagementRate`, `hashtags`, `mentions`, `urls` |
| Source | `mediaSource` (`post`, `quoted post` or `poster image`) |
| Profile | `profileId`, `profileFollowers`, `profileMediaCount` |
| Run | `collectedAt` |

Empty fields are omitted. Status rows carry `status` (`noInput`, `invalidLink`, `invalidUsername`, `notFound`, `rateLimited`, `error`, `noMatchingMedia`, `noRecords`), a human-readable `message`, and the related `tweetUrl`, `tweetId` or `username`.

#### Example record

```json
{
  "type": "media file",
  "tweetId": "2107095768821576019",
  "tweetUrl": "https://x.com/i/status/2107095768821576019",
  "postedAt": "2026-10-05T13:09:13+00:00",
  "mediaType": "video",
  "mediaUrl": "https://video.twimg.com/amplify_video/.../avc1/1920x1080/video.mp4?tag=16",
  "contentType": "video/mp4",
  "mediaId": "2107095744146460672",
  "filename": "2107095768821576019_video_1_1080p.mp4",
  "fileSizeBytes": 3178943,
  "width": 1920,
  "height": 1080,
  "durationSeconds": 13.3,
  "downloadUrl": "https://api.apify.com/v2/key-value-stores/{STORE_ID}/records/...?signature=...",
  "downloadStatus": "finished",
  "uploaderUsername": "NASA",
  "uploaderName": "NASA",
  "uploaderVerified": true,
  "uploaderFollowers": 92310189,
  "likeCount": 7476,
  "retweetCount": 1125,
  "quoteCount": 97,
  "bookmarkCount": 332,
  "viewCount": 2961506,
  "variantQuality": "1080p",
  "isBestQuality": true,
  "mediaSource": "post",
  "collectedAt": "2026-10-29T00:00:00+00:00"
}
```

### Usage cost

You pay the actor's per-result price shown on its Apify page plus normal Apify platform usage. With `saveToFile: false` no media bytes are stored, so a metadata-only run is the cheapest option. Downloads add key-value store usage proportional to the real file sizes. Keep `maxItems` and `maxPostsPerUser` at the values you need — the actor stops as soon as `maxItems` is reached.

### FAQ

**Do I need an X account, cookies or an API key?** No. Everything is read from public, logged-out sources.

**Can I download from a private or protected account?** No. Protected and deleted posts return a `notFound` status row; they are not silently skipped.

**How far back does a profile scan go?** The walk follows X's media timeline page by page until `maxPostsPerUser`, `maxItems` or the end of the timeline. Large profiles can be rate-limited; lower `maxPostsPerUser` and re-run later.

**How are video qualities chosen?** The closest available quality to your preference is used. Portrait videos are compared by their short side, so a 1080×1920 video counts as 1080p. With `downloadAllQualities`, every progressive MP4 becomes its own row and file.

**What happens with quoted posts?** Media inside a quoted post is included by default and marked `mediaSource: "quoted post"`. Turn `includeQuotedMedia` off to keep only the linked post's own media.

**Does audio-only work on GIFs?** No. Animated GIFs have no audio track, so they are skipped in audio-only mode along with photos.

**What if a post has no media?** You get one `noMatchingMedia` status row explaining it, not an empty dataset.

**Do download links expire?** Signed key-value store links follow your Apify storage retention. Re-run the actor or copy the files to your own storage for permanent archiving.

**How do the profile filters treat unknown counts?** A post whose like/repost count or verification status is unknown is kept rather than excluded.

**Can I still use my saved inputs from the previous version?** Yes. Old input keys are mapped automatically in code, so saved tasks keep running.

### Notes

- All timestamps are UTC ISO 8601.
- Engagement rate is derived from likes, reposts, replies and quotes divided by the author's follower count; it is omitted when the follower count is unknown.
- Media URLs are direct X CDN links and open without a Referer header.
- Download links are public signed links tied to the run's key-value store.
- Use the actor only for content you have the right to download.

### Limitations

- Public posts only: deleted posts, protected accounts and login-gated content are not accessible.
- Profile scans cover the most recent media posts X exposes publicly, up to 1,000 posts per run; deeper history is not addressable and very large profiles can hit rate limits, so re-run later if a scan stops early.
- A single media file larger than 1 GB is rejected to keep memory bounded.
- Some media is marked by X as not downloadable; those rows report `downloadStatus: "notDownloaded"` with an explanation.
- Quote media depends on the quoted post still being public.
- X changes its public endpoints periodically; the actor keeps several fallbacks, but a run can still return `rateLimited` or `error` status rows during an outage.

# Actor input Schema

## `postUrls` (type: `array`):

Paste links of posts to download media from. Accepts x.com and twitter.com links, i/web/status links, or bare post IDs. Optional if you provide usernames or profile links below.

## `usernames` (type: `array`):

Scan a profile's media posts and download their photos, videos and GIFs. Type handles with or without @, or paste full profile links. Optional if you provide post links above.

## `maxPostsPerUser` (type: `integer`):

How many of each profile's most recent media posts to look at. Posts without matching media or filtered-out posts are skipped.

## `maxItems` (type: `integer`):

Hard cap on dataset rows for the whole run. The actor stops collecting and downloading once this many files are emitted.

## `mediaTypes` (type: `array`):

Choose which kinds of media to download. All types are selected by default.

## `imageQuality` (type: `string`):

Quality to use for photos (and video poster images). Original is the full-resolution file posted on X.

## `videoQuality` (type: `string`):

Preferred video resolution. The actor picks the closest available quality when the exact one is not offered.

## `downloadAllQualities` (type: `boolean`):

Emit and download one separate file for every progressive MP4 quality X offers (for example 1080p, 720p, 480p) instead of only the preferred one. Every quality gets its own row, filename and download link.

## `includePosterImage` (type: `boolean`):

Save the thumbnail/poster image of each video or GIF as an additional photo file.

## `includeAltText` (type: `boolean`):

Attach the alt text (image description) X stores for each photo or video thumbnail to the media row.

## `audioOnly` (type: `boolean`):

Download only the audio track of videos. Photos and GIFs are skipped. Files are extracted with ffmpeg and file saving is enabled automatically.

## `audioFormat` (type: `string`):

Format used when Audio only is on.

## `includeQuotedMedia` (type: `boolean`):

Also download the photos, videos and GIFs that appear inside a quoted post (a post embedded in the linked post). Those rows are marked with mediaSource "quoted post".

## `includeMetadata` (type: `boolean`):

Attach the post text, date, conversation and reply/quote flags, hashtags, mentions, links, like/repost/reply/quote/bookmark/view counts, author name, avatar, verification and follower count, and a derived engagement rate to every downloaded file.

## `minLikes` (type: `integer`):

When scanning usernames, skip posts with fewer likes than this. Posts whose like count is unknown are kept. Leave at 0 to keep every post.

## `minRetweets` (type: `integer`):

When scanning usernames, skip posts with fewer reposts than this. Posts whose repost count is unknown are kept. Leave at 0 to keep every post.

## `onlyVerifiedAuthors` (type: `boolean`):

When scanning usernames, skip posts whose author is not verified. Posts where verification is unknown are kept.

## `saveToFile` (type: `boolean`):

Download each file into this run's key-value store and add a public signed download link. Turn off to collect metadata and direct CDN links only.

## Actor input object example

```json
{
  "postUrls": [
    "https://x.com/NASA/status/2107095768821576019",
    "2107516524965794217"
  ],
  "usernames": [
    "NASA",
    "https://x.com/elonmusk"
  ],
  "maxPostsPerUser": 1,
  "maxItems": 1,
  "mediaTypes": [
    "video",
    "photo",
    "gif"
  ],
  "imageQuality": "orig",
  "videoQuality": "best",
  "downloadAllQualities": false,
  "includePosterImage": true,
  "includeAltText": true,
  "audioOnly": false,
  "audioFormat": "m4a",
  "includeQuotedMedia": true,
  "includeMetadata": true,
  "minLikes": 0,
  "minRetweets": 0,
  "onlyVerifiedAuthors": false,
  "saveToFile": true
}
```

# Actor output Schema

## `media` (type: `string`):

One row per downloaded video, photo, GIF, audio track or poster image, with media metadata and a public download link. Status rows explain deleted, protected or media-less posts.

## `files` (type: `string`):

Key-value store holding the downloaded media files and their public links.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "postUrls": [
        "https://x.com/NASA/status/2107095768821576019"
    ],
    "usernames": [],
    "maxPostsPerUser": 1,
    "maxItems": 1,
    "mediaTypes": [
        "video",
        "photo",
        "gif"
    ],
    "imageQuality": "orig",
    "videoQuality": "best",
    "downloadAllQualities": false,
    "includePosterImage": false,
    "includeAltText": true,
    "audioOnly": false,
    "audioFormat": "m4a",
    "includeQuotedMedia": true,
    "includeMetadata": true,
    "minLikes": 0,
    "minRetweets": 0,
    "onlyVerifiedAuthors": false,
    "saveToFile": false
};

// Run the Actor and wait for it to finish
const run = await client.actor("dataspryva/twitter-x-media-downloader").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "postUrls": ["https://x.com/NASA/status/2107095768821576019"],
    "usernames": [],
    "maxPostsPerUser": 1,
    "maxItems": 1,
    "mediaTypes": [
        "video",
        "photo",
        "gif",
    ],
    "imageQuality": "orig",
    "videoQuality": "best",
    "downloadAllQualities": False,
    "includePosterImage": False,
    "includeAltText": True,
    "audioOnly": False,
    "audioFormat": "m4a",
    "includeQuotedMedia": True,
    "includeMetadata": True,
    "minLikes": 0,
    "minRetweets": 0,
    "onlyVerifiedAuthors": False,
    "saveToFile": False,
}

# Run the Actor and wait for it to finish
run = client.actor("dataspryva/twitter-x-media-downloader").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "postUrls": [
    "https://x.com/NASA/status/2107095768821576019"
  ],
  "usernames": [],
  "maxPostsPerUser": 1,
  "maxItems": 1,
  "mediaTypes": [
    "video",
    "photo",
    "gif"
  ],
  "imageQuality": "orig",
  "videoQuality": "best",
  "downloadAllQualities": false,
  "includePosterImage": false,
  "includeAltText": true,
  "audioOnly": false,
  "audioFormat": "m4a",
  "includeQuotedMedia": true,
  "includeMetadata": true,
  "minLikes": 0,
  "minRetweets": 0,
  "onlyVerifiedAuthors": false,
  "saveToFile": false
}' |
apify call dataspryva/twitter-x-media-downloader --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,dataspryva/twitter-x-media-downloader"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/fnHBLqMkGad5UusrY/builds/b2lfr5bXEkHVsmX4j/openapi.json
