MEDIA TAB AS JSON
Twitter Media Scraper API: Pull Photo and Video URLs
Updated July 2026
How do you scrape Twitter media through an API?
Call GET /twitter/user/media with a handle or user ID and you get that account's Media tab as JSON: the tweets, each with its photo or video entries under extended_entities.media, plus a cursor for the next page. Each call costs $0.0008, returns up to 100 items, and needs no browser, no proxy and no X login.
$0.0008 per call. About $0.048 per 1,000 posts at the fill we measure on the nearest timeline route, and $0.008 on full 100-item pages.
Per our own API records, 125 endpoints share this one Bearer key and one prepaid balance. Start with the quickstart, compare every price, or connect an agent through the MCP server.
By the numbers
What a media crawl costs at real fill
Measured on 484,424 paying calls, October 22 to 28, 2026 (UTC). The media route is a timeline read, so the timeline figure is the nearest published fill.
Paying calls to user/tweets returned 16.74 tweets per call on average, with 1.6% of calls empty. (TwitterAPIs measured yield, 2026)
That fill works out to $0.048 per 1,000 tweets delivered, empty calls included. (TwitterAPIs measured yield, 2026)
The official X API bills reads at $0.005 per resource, which is $5 per 1,000 posts. (X API docs, 2026)
The endpoint that reads an account's Media tab
This route returns what you see on an account's Media tab, in order, a page at a time. Give it one identifier, either username or user_id, and raise count above its default of 20 when you are crawling. The parameter table is generated from our OpenAPI document.
GET /twitter/user/media
Fetch the media tweets posted by a user, cursor-paginated. Returns the tweet objects from the account's Media tab, with media URLs included under extended_entities.media. The tab occasionally surfaces a link-or-card tweet with no attachment, so guard on extended_entities before reading it.
| Parameter | Where | Required | Purpose |
|---|---|---|---|
| username | query | Yes | Handle without the leading @. |
| user_id | query | No | Numeric user ID; provide either username or user_id. |
| cursor | query | No | Pagination cursor from a previous response. |
| count | query | No | Max items to return for this page. Default 20; clamped to 1-100. |
Sample response from GET /twitter/user/media
Each item is a normal tweet object with its attachments under extended_entities.media. The tab occasionally surfaces a link or card post with no attachment, so guard on that field before you read it. That single check is the difference between a crawl that finishes and one that throws on page 40.
{
"tweets": [
{
"id": "1759987654321000111",
"text": "A new image of the Pillars of Creation.",
"extended_entities": {
"media": [
{
"type": "photo",
"media_url_https": "https://pbs.twimg.com/media/abc.jpg",
"url": "https://t.co/abc123",
"ext_alt_text": "The Pillars of Creation imaged by JWST."
}
]
},
"author": {
"id": "11348282",
"username": "nasa"
}
}
],
"next_cursor": "DAABCgABF..."
}| Field | What it tells you |
|---|---|
| tweets[].id | The post the media belongs to, so you can link back to it or read its counts later. |
| tweets[].text | The caption, often the only description of the image you will get. |
| extended_entities.media[].type | photo, video or animated_gif. Branch on this before you download anything. |
| extended_entities.media[].media_url_https | A direct HTTPS URL for the image, or the poster frame on a video. |
| extended_entities.media[].ext_alt_text | The alt text the author wrote, when there is one. Useful as a caption or a training label. |
| next_cursor | Pass this back as cursor for the next page. No cursor means you reached the end. |
Quickstart: pull an account's media as JSON
This is a read, so a Bearer key is all you need. There is no X account to connect, no cookie to refresh and no browser to keep alive. The request below fetches one page, and a loop over next_cursor turns it into a full crawl.
- Create a key at signup. The key works straight away.
- Send the request below with a handle in
username. - Read
next_cursorfrom the response and repeat withcursorset to it until no cursor comes back. - Guard on
extended_entitiesbefore reading media, then download eachmedia_url_httpsyou want to keep.
curl "https://api.twitterapis.com/twitter/user/media?username=nasa" \
-H "Authorization: Bearer $TWITTERAPIS_KEY"import os
import requests
resp = requests.get(
"https://api.twitterapis.com/twitter/user/media",
headers={"Authorization": f"Bearer {os.environ['TWITTERAPIS_KEY']}"},
params={"username": "nasa"},
)
resp.raise_for_status()
data = resp.json()
print(data)const url = new URL("https://api.twitterapis.com/twitter/user/media");
url.search = new URLSearchParams({ username: "nasa" }).toString();
const resp = await fetch(url, {
headers: { Authorization: `Bearer ${process.env.TWITTERAPIS_KEY}` },
});
const data = await resp.json();
console.log(data);What a Twitter media scrape costs per 1,000 posts
You pay per call, so the price per post depends on how full each page is. The table gives you three honest readings: a full page at the maximum count, a page at the default count, and the fill we actually measure across paying timeline traffic, which already includes empty calls.
| Reading | Posts per call | Per 1,000 posts |
|---|---|---|
| Full pages at count=100 | 100 | $0.008 |
| Full pages at the default count of 20 | 20 | $0.04 |
| Measured fill on user/tweets, empty calls included | 16.74 | $0.048 |
Measured over October 22 to 28, 2026 (UTC) on paying calls only. The media route is not sampled on its own, so the last row is the nearest timeline read rather than this endpoint's own figure. The signup credit of $0.50 covers 625 calls.
Ways to scrape Twitter media compared
Most media scrapers on the first page of Google are browser extensions, open-source libraries or general scraping actors. They all work for a one-off, and they differ in what they ask from you once the job runs every day. This is how the common routes stack up.
| Option | Cost | What you need | Where it breaks |
|---|---|---|---|
| TwitterAPIs | $0.0008 per call | One Bearer key | An account going private or suspended |
| Official X API | $0.005 per post read | Developer app and prepaid credits | The bill on a deep account history |
| Browser extensions | Free to low monthly | Your own logged-in browser | Unattended runs, and your session is on the line |
| Open-source libraries | Free plus proxies | Your own X accounts in a pool | Whenever X changes its internal API |
Photos, videos and GIFs in one feed
The Media tab mixes every attachment type, so your crawler should branch on type first. Photos are the simple case: media_url_https is the file. Add ?name=orig to a photo URL to request the largest size X stores, which is what you want for an archive and what you do not want for a thumbnail grid.
Videos and GIFs need one more step, because media_url_https is only the poster frame on those. The playable files sit in the video variants of the same media entry, and our answer on downloading a video from a tweet shows how to pick the right one. To get image URLs out of tweets you already hold, the guide to extracting image URLs covers that path.
Who reads the Media tab through an API
A media crawl is a small part of many different products, and the shared requirement is that it runs unattended. A script that needs someone's laptop open is fine once, and it stops being fine the day the job goes on a schedule. These are the jobs we see it used for.
- Brand and rights monitoring. Collect what an account has published, then compare images against your own assets.
- Dataset building. Pair each image with its caption and alt text for labelled training data. Read our notes on using the API for AI training data first.
- Creator tools. Show a creator their own media history, with engagement from a tweet lookup beside each item.
- Archiving. Keep a copy of a public account's images before an account closes, one cursor page at a time.
How to crawl a large account without surprises
Store the cursor after every page, so a crash resumes where it stopped instead of paying for the same pages twice. Download files in a separate step from the API crawl, because the two fail for different reasons and you want to retry them independently. Files from pbs.twimg.com do not count against your API balance at all.
One flat ceiling of 600 requests per minute per key applies, which is far more than a single account needs. If you crawl many accounts, keep concurrency under the limit of 20 per key and back off on a 429. For text as well as media, the timeline API gives you every post rather than only those with attachments.
A complete crawl in about twenty lines
The loop below walks one account's Media tab from newest to oldest. It requests the maximum page size, skips any item with no attachment, writes out each media URL, and stops when no cursor comes back. Saving the cursor between runs is left out for clarity, and it is the first thing to add for an account with a long history.
Run it for a test account first and look at the output before you schedule anything. Accounts differ a lot in how much media they post, and a quick look tells you whether you need the full history or only the last few pages. For most monitoring jobs, a daily run that stops at the first post you have already seen is both cheaper and simpler than a full crawl.
import os
import requests
API = "https://api.twitterapis.com/twitter/user/media"
HEADERS = {"Authorization": f"Bearer {os.environ['TWITTERAPIS_KEY']}"}
params = {"username": "nasa", "count": 100}
while True:
resp = requests.get(API, headers=HEADERS, params=params, timeout=30)
resp.raise_for_status()
page = resp.json()
for tweet in page["tweets"]:
entities = tweet.get("extended_entities")
if not entities:
continue
for item in entities["media"]:
print(tweet["id"], item["type"], item["media_url_https"])
if not page.get("next_cursor"):
break
params["cursor"] = page["next_cursor"]Choosing between the media, timeline and scraper routes
Several routes can return an account's posts, and the right one depends on what you want to keep. The Media tab is the efficient choice when you only care about attachments, because a timeline crawl spends calls on text posts you would throw away. Reach for one of the others when your job needs more than the images themselves.
If you are still comparing vendors rather than routes, our ranking of the best Twitter scraping APIs prices the main options side by side, and the answer on whether you need proxies to scrape Twitter explains why an API removes that problem. All of them return the same JSON shape for tweets.
- Every post from an account. Use the timeline API, which includes text-only posts and replies.
- Keyword or hashtag collection. Use the search API, which works across all accounts rather than one.
- General tweet collection. Start with the Twitter scraper API page, which covers the broader job.
- Profile data. Follower counts and bios come from the user API, one call per account.
Rights, storage and what you may do with the files
Reading a public image and owning it are different things. The author keeps the copyright in what they post, so a crawl gives you access to the file and not a licence to republish it. Monitoring, research and internal analysis sit in a very different place from reposting someone's photo in your own campaign, and you should get advice on anything commercial. Our answer on whether it is legal to scrape Twitter lays out the main questions.
If you keep files, store the tweet ID and author beside each one so you can honour a takedown later. Hotlinking pbs.twimg.com URLs in a product is fragile, because a file can disappear when the post is deleted, so copy what you need into your own storage and record where it came from.
Reading media from one post instead of a whole account
When you already know the post, skip the crawl. A tweet by ID lookup returns the attached media on that single post, which is the cheaper route for a link a user pasted. Use the Media tab when the account is the unit of work, and use a lookup when the post is.
Pull media from an AI agent
Ask an agent what an account has been posting visually and it can read the Media tab directly through the tool below. The tool takes the same username, user_id, cursor and count inputs as the REST call, and the MCP page has the setup for each client.
twitter_user_mediacalls GET /twitter/user/media
npx -y @twitterapis/mcp@latestTwitter media scraper questions
Call GET /twitter/user/media with the handle in username, then loop on next_cursor. Each tweet carries its images under extended_entities.media, and media_url_https is the direct file. Each page costs $0.0008.
Up to 100. The default count is 20 and the value is clamped between 1 and 100. A page can come back with fewer, so always drive the loop from next_cursor rather than from the page size.
On a video or GIF entry, media_url_https is only the poster frame. Read the video variants on the same media entry and choose by bitrate. The answer on downloading a video from a tweet shows the exact field.
No. This reads public accounts. A protected account's Media tab is only visible to its approved followers.
No. It is a plain REST read on a Bearer key. You do not supply cookies, run a headless browser or manage proxies.
Append ?name=orig to the media_url_https value. X serves the largest stored size for that photo.
The Media tab occasionally surfaces a link or card post with no attachment. Skip any item without extended_entities rather than treating it as an error.
About $0.48 at the fill we measure on timeline reads, and $0.08 if every page returns the full 100 items.
Crawl your first Media tab in a minute
Create a key and send one request. No browser, no cookies and no X developer account.
Next read
Continue exploring related pages:
Twitter API use cases
14 use cases from sentiment analysis to lead generation.
Twitter API answers
Short, sourced answers to the most common Twitter and X API questions.
Twitter timeline API
Pull an account's full posting history over one REST endpoint.
Twitter list API
Read list members and list timelines with the same Bearer key.