# X (Twitter) Media Scraper | $0.40/1K | Photos and Videos (`feedminer/x-media-scraper`) Actor

Export photos, videos and gifs from X (Twitter) accounts or posts. One row per attachment with every video encoding, bitrate, dimensions, duration, alt text, preview image, and the parent post with its author and engagement. Direct links, ready to download, as JSON, CSV or Excel. By FeedMiner.

- **URL**: https://apify.com/feedminer/x-media-scraper.md
- **Developed by:** [FeedMiner](https://apify.com/feedminer) (community)
- **Categories:** Social media, Videos, Developer tools
- **Stats:** 1 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.32 / 1,000 media returneds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
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?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

<div style="border-left:4px solid #1D9BF0;background:#F2F9FF;padding:14px 18px;border-radius:6px">
<strong>Export every photo, video and gif from X (Twitter) accounts or posts.</strong> One row per file, with every video encoding X offers, its bitrate, dimensions, duration, alt text, and the post it came from. Direct links, ready to download.
</div>

🎞️ **Every encoding, not just one.** A video comes back with its full variant list, best first: the highest bitrate for archiving, the smallest for a preview. Tools that return a single link decide for you.

💸 **$0.40 per 1,000 files, down to $0.32 on the larger Apify plans.** The visible alternatives charge $2.99 and $5.00 per 1,000 for the same links, plus a start fee on every run. No start fee here.

🖼️ **The post travels with the file.** Each row carries the parent post, its date, its author and its engagement, so a media export doubles as a content performance table.

♿ **Alt text included.** The accessibility description creators write is on the row, which is the field content teams and dataset builders ask for first.

#### Copy to your AI assistant

```
feedminer/x-media-scraper on Apify. Exports photos, videos and gifs from X (Twitter) accounts or individual posts. One row per attachment with id, type, url (best encoding), previewUrl, contentType, bitrate, width, height, durationMs, altText, variants[] and the parent post (tweetId, tweetUrl, text, createdAt, likeCount, viewCount, author). Call ApifyClient("TOKEN").actor("feedminer/x-media-scraper").call(run_input={...}), then client.dataset(run["defaultDatasetId"]).list_items().items. Input: handles (string[]), startUrls (string[] of post URLs or ids), mediaTypes (string[] of photo|video|gif), maxItems (int), sessions (array, optional). Full spec: GET https://api.apify.com/v2/acts/feedminer~x-media-scraper/builds/default (Bearer TOKEN) → inputSchema, actorDefinition.storages.dataset, readme. Token: https://console.apify.com/account/integrations
```

***

### <img src="https://apify-image-uploads-prod.s3.us-east-1.amazonaws.com/hoZ24gXinDHziMQKm-actor-3hbiY2nfMGfbNdh1w-DUuuMSYQpE-IMG_9471.jpeg" width="26" height="26" style="vertical-align:-4px;border-radius:6px"/> How to scrape media from X (Twitter)

##### Basic: everything an account posted

```json
{ "handles": ["nasa"], "maxItems": 200 }
```

##### Videos only, for a highlight reel

```json
{ "handles": ["nasa"], "mediaTypes": ["video"], "maxItems": 50 }
```

##### A single post you already have

```json
{ "startUrls": ["https://x.com/NASA/status/2089425053599289546"] }
```

##### A batch of posts from another run

```json
{
  "startUrls": [
    "https://x.com/NASA/status/2089425053599289546",
    "https://x.com/NASA/status/2089802824678879526"
  ],
  "maxItems": 100
}
```

Pasted posts are read together, so a hundred URLs cost one or two requests rather than a hundred.

##### Photos from several accounts, for a brand asset sweep

```json
{ "handles": ["stripe", "adyen"], "mediaTypes": ["photo"], "maxItems": 400 }
```

***

### Input parameters

| Parameter | Type | Default | Description |
|---|---|---|---|
| `handles` | array |  | Accounts whose media you want, as handles or profile URLs |
| `startUrls` | array |  | Individual posts to read, as post URLs or ids |
| `mediaTypes` | array |  | Leave empty for everything |
| `maxItems` | integer | `1000` | Stop once this many attachments have been collected |

**Advanced**

| Parameter | Type | Default | Description |
|---|---|---|---|
| `sessions` | array |  | Optional. The Actor works without one. X accounts of your own, for anyone who would rather the run used accounts they control |
| `debugMode` | boolean | `false` | Verbose logs for support |

#### Coming from another media downloader

`tweetUrls`, `links`, `urls` and `postUrls` are all read as posts, and `onlyVideos` or `onlyImages` select the kind. The input you already send elsewhere works unchanged.

#### What an account changes here

Nothing about what you get, only how fast. An account makes an account's media tab answer straight away; without one the same rows still come back, they just take longer.

***

### What data can you extract from X media?

| Field | Type | Description |
|---|---|---|
| `id` | string | Media id |
| `type` | string | `photo`, `video` or `animated_gif` |
| `url` | string | Direct link to the file. For a video, the best encoding |
| `previewUrl` | string | Still image, the poster frame for a video |
| `contentType` | string | MIME type of `url`, for example `video/mp4` |
| `bitrate` | integer | Bitrate of `url`, in bits per second |
| `width` `height` | integer | Original dimensions |
| `durationMs` | integer | Video duration in milliseconds |
| `altText` | string | Accessibility description written by the author |
| `variants` | array | Every encoding, best first, each with `url`, `bitrate`, `contentType` |
| `tweetId` | string | Parent post id |
| `tweetUrl` | string | Parent post link |
| `text` | string | Parent post text |
| `createdAt` | string | Parent post date, RFC 3339 |
| `likeCount` `viewCount` | integer | Parent post engagement |
| `author` | object | Handle, name, bio, followers, verification, avatar |

#### Example output

```json
{
  "id": "2089424963400818688",
  "type": "video",
  "url": "https://video.twimg.com/amplify_video/2089424963400818688/vid/avc1/1920x1080/xyz.mp4",
  "previewUrl": "https://pbs.twimg.com/media/HP8hAu4WEAAJLH7.png",
  "contentType": "video/mp4",
  "bitrate": 10368000,
  "width": 1920,
  "height": 1080,
  "durationMs": 45120,
  "altText": "Artemis II crew waves before boarding",
  "variants": [
    { "url": "...1920x1080...", "bitrate": 10368000, "contentType": "video/mp4" },
    { "url": "...1280x720...", "bitrate": 2176000, "contentType": "video/mp4" }
  ],
  "tweetId": "2089425053599289546",
  "tweetUrl": "https://x.com/NASA/status/2089425053599289546",
  "author": { "userName": "NASA", "followers": 79000000 }
}
```

***

### Column mapping, if you are switching

| Here | igview |
|---|---|
| `url` | `downloadUrl` |
| `previewUrl` | `thumbnail` |
| `type` | `mediaType` |
| `durationMs` | `video.duration` |
| `variants` | `video.quality`, one value there, the whole ladder here |
| `tweetUrl` | `tweetUrl` |
| `text` | `text` |
| `author.userName` | `username` |
| `author.name` | `name` |

The difference that matters is `variants`: one encoding there, every encoding X
offers here, best first, so archiving and previewing come from the same row.

### Pricing: Pay Per Event (PPE)

| Event | Price |
|---|---|
| Media returned | $0.0004 |

That is **$0.40 per 1,000 files**, with no start fee and no charge for a run that returns nothing. The price steps down on its own for the larger Apify plans, to $0.36 and then $0.32 per 1,000.

***

##### What a run costs

| | |
|---|---|
| An account's media tab, 600 files | $0.24 |
| A 5,000 file archive | $2.00 |

##### Free tier

The Apify free plan includes $5 of usage every month, which is roughly 12,000 files here. No card required.

### Advanced usage

**Pick your own quality.** `url` is the best encoding, and `variants` holds the rest. Archiving takes the first, a preview grid takes the last, and a bandwidth budget takes the one under your bitrate ceiling.

**Build a training set with captions.** `altText` plus `previewUrl` is an image and description pair, straight out of the export.

**Rank creative by performance.** Every row carries the parent post's likes and views, so sorting media by `viewCount` tells you which visual actually worked.

**Four photos in one post stay four rows.** A gallery is not collapsed to its first image, which is where several tools quietly lose three quarters of the files.

**Links have a lifetime.** X's file links are served from its own CDN and are stable for a long while, not forever. Download what you need close to the run rather than storing links for months.

**A pasted post always answers.** When X's usual route refuses a post, the run reads it through the same public endpoint that powers embedded posts on other websites. The files and their encodings come back identical; the parent post's view count is the only field that endpoint does not publish, and it stays at zero rather than being guessed.

***

### Integrate X media into your stack

**Python**

```python
from apify_client import ApifyClient
import requests

client = ApifyClient("YOUR_APIFY_TOKEN")
run = client.actor("feedminer/x-media-scraper").call(run_input={
    "handles": ["nasa"],
    "mediaTypes": ["video"],
    "maxItems": 25,
})

for row in client.dataset(run["defaultDatasetId"]).list_items().items:
    with open(f"{row['id']}.mp4", "wb") as f:
        f.write(requests.get(row["url"]).content)
```

**JavaScript**

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

const client = new ApifyClient({ token: 'YOUR_APIFY_TOKEN' });
const run = await client.actor('feedminer/x-media-scraper').call({
    handles: ['nasa'],
    maxItems: 100,
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
const light = items.map((m) => m.variants?.at(-1)?.url ?? m.url);
```

**No code**

Push rows to Google Sheets, Airtable, Slack, Zapier, Make or n8n from the Integrations tab of the run.

***

### Performance

| Measure | Value |
|---|---|
| Long lists of links | A hundred pasted post links cost barely more time than one |
| Depth | 626 files from one large account's media tab, measured |
| Parallelism | Up to four accounts at a time |
| Partial results | Kept and delivered when a limit is reached |

***

### FAQ

**Does it download the files?**
It returns direct links to them, which is what an integration, a downloader or a browser needs. Downloading is three lines in the Python example above.

**Why does a video have several links?**
X encodes each video at several qualities. `url` is the best one, `variants` is the full list, best first.

**Are the links permanent?**
They are served by X's CDN and stay valid for a long time, not forever. Fetch what you keep close to the run.

**Do I get media from replies too?**
The media tab of an account is what X publishes as its media. Point `startUrls` at a specific post to get exactly that post's attachments.

**Can I filter by date?**
Not in this actor. Use X Advanced Search Scraper with `since` and `until` to get the post URLs, then paste them here.

**What about a protected account?**
Its media is not public, and the run reports it instead of failing.

**Is alt text always there?**
Only when the author wrote one. The field is empty otherwise, never invented.

**Can I get a thumbnail for a photo?**
`previewUrl` is the image itself for a photo, and the poster frame for a video.

***

### Support

A file you cannot reach, a variant that looks wrong, a format you need: write from the actor page.

***

### Legal compliance

This actor reads media that X publishes publicly. It does not access private accounts or direct messages. Media is copyrighted by its authors: exporting links for analysis, monitoring or archiving is not a licence to republish. Personal data in the output is subject to GDPR, CCPA and similar regimes.

***

**FeedMiner** · Fast, honestly priced scrapers, kept alive when platforms change.

[X Tweet Scraper](https://apify.com/feedminer/x-tweet-scraper) · [X Profile Scraper](https://apify.com/feedminer/x-profile-scraper) · [X Follower Scraper](https://apify.com/feedminer/x-follower-scraper) · [X Advanced Search Scraper](https://apify.com/feedminer/x-advanced-search-scraper) · X Media Scraper · [X Mentions Monitor](https://apify.com/feedminer/x-mentions-scraper) · [X Engagement Scraper](https://apify.com/feedminer/x-engagement-scraper) · [X Likes and Reposts Scraper](https://apify.com/feedminer/x-signals-scraper) · [X Trends Scraper](https://apify.com/feedminer/x-trends-scraper) · [All FeedMiner Actors](https://apify.com/feedminer)

# Actor input Schema

## `handles` (type: `array`):

Accounts whose media you want, as handles or profile URLs.

## `startUrls` (type: `array`):

Individual posts to read, as post URLs or ids.

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

Leave empty for everything.

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

Stop once this many attachments have been collected.

## `sessions` (type: `array`):

Optional. The Actor works without one. X accounts of your own, for anyone who would rather the run used accounts they control.

## `debugMode` (type: `boolean`):

Verbose logs for support.

## Actor input object example

```json
{
  "handles": [
    "nasa"
  ],
  "maxItems": 1000,
  "debugMode": false
}
```

# Actor output Schema

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

No description

# 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 = {
    "handles": [
        "nasa"
    ],
    "maxItems": 1000
};

// Run the Actor and wait for it to finish
const run = await client.actor("feedminer/x-media-scraper").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 = {
    "handles": ["nasa"],
    "maxItems": 1000,
}

# Run the Actor and wait for it to finish
run = client.actor("feedminer/x-media-scraper").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 '{
  "handles": [
    "nasa"
  ],
  "maxItems": 1000
}' |
apify call feedminer/x-media-scraper --silent --output-dataset

```

## MCP server setup

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

```

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/CrPZmLxFE32UibRPc/builds/y5vyV6Fgq9HpsZzwG/openapi.json
