Skip to content

Twitter image scraper: download every image

A Twitter image scraper finds the posts of an account or a search that carry photos, reads each photo's pbs.twimg.com link and downloads it with name=orig, because the bare link X serves stops at 1,200 pixels wide.

Short answer: Logged out, x.com shows only a few of a profile's newest posts, so browser extensions and downloaders read X through your login or session cookie. To collect every image without risking your account, use Sorsa API: POST /search-tweets with filter:images returns about 20 image posts per request, from $0.07 per 1,000, and 100 free requests cover the first 2,000.

Endpoint
POST /search-tweets
Page size
about 20 posts per request
Price
from $0.07 per 1,000

#How a Twitter image scraper works

Every Twitter image scraper does the same three things, whatever it runs on. For a single post you need none of them: paste its link into the free Media Downloader and save each photo at full size. For hundreds or thousands of images, the job looks like this:

  1. Find the posts that carry photos. X's search operator filter:images keeps only posts with at least one image, from one account with from: or for any topic.

  2. Read each photo's link. Every image sits on X's media server at a link such as https://pbs.twimg.com/media/G4kQ2vXbIAAm7Rz.jpg.

  3. Download the original. Request that link with ?format=jpg&name=orig; without it, X sends a copy that fits 1,200 pixels.

Tools differ in step 1, in where the posts come from. x.com shows a logged-out visitor single posts and a few of a profile's newest posts, and sends search to its login page. Browser extensions, desktop downloaders and command-line tools therefore read X through a logged-in account. Their own documentation says so: one widely used batch downloader asks for your auth_token cookie and warns that it can get the account suspended.

Only an API reaches a whole account's images: the official X API with a developer account and OAuth, or a Twitter/X data API with one key.

The guide on how to scrape Twitter covers every route and what breaks it, and the roundup of the best Twitter scrapers prices the tools one by one.

#Download every image from an X account

Sorsa API, a Twitter (X) API and scraper, returns an account's image posts as JSON with each photo's link inside, about 20 posts per request and back to 2006. Four steps take you from a handle to a folder of original files.

  1. Get a key. Sign up and copy the API key from your dashboard. The first 100 requests are free, no card needed.

  2. Search the account's image posts. Send from: plus filter:images to POST /search-tweets (opens in a new tab), with order set to latest for newest first.

    Terminal
    curl -X POST "https://api.sorsa.io/v3/search-tweets" \
      -H "ApiKey: YOUR_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{"query": "from:northfield_photo_lab filter:images",
           "order": "latest"}'
    
    Response200 OK
    {
      "tweets": [
        {
          "id": "1984213377309245441",
          "created_at": "2026-10-30T14:12:09Z",
          "full_text": "Fog over the north ridge this morning.",
          "likes_count": 412,
          "entities": [
            {
              "type": "photo",
              "link": "https://pbs.twimg.com/media/G4kQ2vXbIAAm7Rz.jpg",
              "preview": "https://pbs.twimg.com/media/G4kQ2vXbIAAm7Rz.jpg"
            },
            {
              "type": "photo",
              "link": "https://pbs.twimg.com/media/G4kQ2vZaQAEw9Lp.jpg",
              "preview": "https://pbs.twimg.com/media/G4kQ2vZaQAEw9Lp.jpg"
            }
          ],
          "user": {
            "username": "northfield_photo_lab",
            "display_name": "Northfield Photo Lab",
            "media_count": 1840
          }
        }
      ],
      "next_cursor": "DAADDAABCgABG8vZ0Qm__-oKAAIbuv3MBVvA0AgAAwAAAAIAAA"
    }
    
  3. Follow the cursor. A cursor is a token that marks where the next page starts: send next_cursor back in the body of the next request until it comes back empty.

    From a search query to original-size image files 1. Query from:account filter:images one account or topic 2. Search page /search-tweets about 20 posts per request 3. Photo links type: photo pbs.twimg.com 1 to 4 per post 4. Save name=orig original file <post id>_1.jpg next_cursor, until empty + a row in images.csv
    Each request returns about 20 image posts and a cursor to the next page; every photo link becomes an original-size file and a row in images.csv.
  4. Save each photo at its original size. The script keeps only photo entities, rewrites each link to name=orig and names files <post id>_<n>.jpg. It logs every file in images.csv and stores the cursor after each page, so a stopped run picks up where it left off.

    import csv
    import os
    import sys
    import time
    from urllib.parse import parse_qs, urlparse
    
    import requests
    
    API = "https://api.sorsa.io/v3/search-tweets"
    HEADERS = {"ApiKey": "YOUR_API_KEY"}
    
    
    def orig(url):
        """Rewrite a pbs.twimg.com image link to its original size."""
        u = urlparse(url)
        path = u.path
        fmt = parse_qs(u.query).get("format", ["jpg"])[0]
        if "." in path.rsplit("/", 1)[-1]:
            path, fmt = path.rsplit(".", 1)
        fmt = fmt.split(":")[0]
        return f"https://pbs.twimg.com{path}?format={fmt}&name=orig", fmt
    
    
    def photo_urls(tweet):
        """Original-size links of every photo in a post."""
        for e in tweet.get("entities") or []:
            if e.get("type") != "photo":
                continue
            for link in (e.get("link"), e.get("preview")):
                if link and "pbs.twimg.com/media/" in link:
                    yield orig(link)
                    break
    
    
    def post(body):
        while True:
            r = requests.post(API, headers=HEADERS, json=body, timeout=60)
            if r.status_code == 429:  # over 20 requests a second: wait
                time.sleep(1)
                continue
            r.raise_for_status()  # 403: no key, or no requests left
            return r.json()
    
    
    def download(query, folder):
        os.makedirs(folder, exist_ok=True)
        state = os.path.join(folder, "cursor.txt")
        body = {"query": query, "order": "latest"}
        if os.path.exists(state):  # resume a stopped run
            body["next_cursor"] = open(state).read().strip()
        log_path = os.path.join(folder, "images.csv")
        with open(log_path, "a", newline="", encoding="utf-8") as f:
            log = csv.writer(f)
            while True:
                page = post(body)
                for t in page.get("tweets", []):
                    for n, (url, fmt) in enumerate(photo_urls(t), 1):
                        path = os.path.join(folder, f"{t['id']}_{n}.{fmt}")
                        if os.path.exists(path):
                            continue
                        img = requests.get(url, timeout=60)
                        img.raise_for_status()
                        with open(path, "wb") as out:
                            out.write(img.content)
                        log.writerow([t["id"], t["created_at"], url, path])
                cursor = page.get("next_cursor")
                if not cursor:
                    break
                body["next_cursor"] = cursor
                with open(state, "w") as s:
                    s.write(cursor)
        if os.path.exists(state):
            os.remove(state)
    
    
    if __name__ == "__main__":
        download(sys.argv[1], sys.argv[2])
    

Run it with a search and a folder name: python twitter_images.py "from:northfield_photo_lab filter:images" northfield_photo_lab. A 403 response means the key is missing or out of requests; a 429 means more than 20 requests in one second, and the script waits a second and tries again. Files already on disk are skipped, so running the same command again only adds new images.

Good to know

A quote or a retweet carries someone else's photos: they sit in quoted_status.entities and retweeted_status.entities, and the script leaves them out. Add those two lists to photo_urls to keep them. To size the job first, GET /info (opens in a new tab) returns the account's media_count, the photos and videos it has posted.

To walk the account's timeline post by post instead of searching it, send the handle to POST /user-tweets (opens in a new tab). It returns the same tweet objects, 20 per request, so photo_urls reads their images unchanged; text-only posts come along too, so the job takes more requests.

The same script collects the images of any topic: only the query changes. filter:images combines with every X search operator, and search reaches back to 2006.

JobQuery
A hashtag#nightphotography filter:images
A keyword in one monthaurora filter:images since:2026-10-01 until:2026-11-01
Popular images only#interiordesign filter:images min_faves:500
An account without its repliesfrom:northfield_photo_lab filter:images -filter:replies
Several accounts at once(from:northfield_photo_lab OR from:harborline_studio_co) filter:images
  • A hashtag

    Query
    #nightphotography filter:images
  • A keyword in one month

    Query
    aurora filter:images since:2026-10-01 until:2026-11-01
  • Popular images only

    Query
    #interiordesign filter:images min_faves:500
  • An account without its replies

    Query
    from:northfield_photo_lab filter:images -filter:replies
  • Several accounts at once

    Query
    (from:northfield_photo_lab OR from:harborline_studio_co) filter:images

For a big topic, run one date window per month in its own folder. Each run stays short, a failed one is easy to repeat, and the windows run side by side on one key, up to 20 requests per second on every plan. Set order to popular for X's top posts first or latest for the newest. Every operator, with examples, is in the guide to Twitter search operators.

The images of a hashtag change as people post, so a monitoring job reruns the newest window on a schedule. Because the script skips files it already has and logs only new ones, each rerun adds just the images posted since the last.

#Get every image at its original size

X keeps each upload whole but serves smaller copies, and the copy you get depends only on the name value in the link. On November 2, 2026 we requested four public images in every size X serves and measured what came back; the ladder below is a photo posted in late October 2026.

Width X returned for one photo, by name value Width X returned for one photo, by the name value in its link name=thumb 150 px7 KB name=small 680 px53 KB bare link (medium) 1,200 px143 KB name=large 2,048 px351 KB name=4096x4096 4,096 px1.03 MB name=orig 8,000 px1.95 MB The uploaded file was 8,000 × 4,501 px. Measured on pbs.twimg.com, November 2, 2026.
Only name=orig returned the uploaded file: 8,000 pixels wide and 1.95 MB, against 1,200 pixels and 143 KB from the bare link.
  • The bare link is the medium copy. A pbs.twimg.com link with no name returns an image that fits 1,200 pixels.
  • large stops at 2,048 pixels and 4096x4096 at 4,096. An upload smaller than that comes back at its own size.
  • name=orig returns the upload itself. For the photo above that meant 8,000 × 4,501 pixels, 44 times the pixels of the bare link.
  • format=png adds no detail. It re-encodes a JPG upload into a PNG several times larger, and format=png&name=orig returns 404 for a JPG, so keep the format the link already has.

Downloading from pbs.twimg.com is a plain HTTP request with no key, so the files themselves cost no API requests: only finding the posts does.

#Bulk-download images without code

Without code, the job splits in two: export the image links, then download the files.

  1. Export the links of recent posts. The free Export Tweets tool saves an account's latest posts as CSV, Excel or JSON with a media_urls column; switch on Only posts with media.

  2. Export a whole account or search. In the API playground, run POST /search-tweets with your key and a query such as from:northfield_photo_lab filter:images; it follows the cursor for you and exports every page to CSV or JSON.

  3. Turn the links into originals. Paste the image links into the converter below and copy the original-size links it returns.

  4. Download the list. Load the links into any download manager that accepts a list of URLs, and it saves the files in one run.

    Paste any number of pbs.twimg.com links, one per line or straight from a CSV column; nothing leaves your browser.

#What a bulk image download costs

On Sorsa API, image posts cost from $0.07 per 1,000, because a search page of about 20 posts is one request however many photos it holds. On the official X API, image links come only through its media expansion. From November 22, 2026, X bills each expanded image (opens in a new tab) at $0.005 on top of the $0.005 post read: $10.00 per 1,000 posts with one image, $25.00 when each carries four.

Set side by side, the ways to get the posts differ in what they need from you as much as in price.

Way to get the postsWhat it needsCost per 1,000 image posts
Sorsa APIOne API keyfrom $0.07
Official X APIDeveloper account and OAuth$10.00 from Nov 22, 2026 (post and one image)
Extensions and downloadersYour X login or session cookieFree or a subscription, with the account at risk
  • Sorsa API

    What it needs
    One API key
    Cost per 1,000 image posts
    from $0.07
  • Official X API

    What it needs
    Developer account and OAuth
    Cost per 1,000 image posts
    $10.00 from Nov 22, 2026 (post and one image)
  • Extensions and downloaders

    What it needs
    Your X login or session cookie
    Cost per 1,000 image posts
    Free or a subscription, with the account at risk
Image posts

Official X API

$10,000

for 1M image posts

$0.010 per post with one image, from Nov 22

Sorsa API

from$70

for 1M image posts

from $0.07 per 1,000

140x cheaper

The official X API's pay-per-use pricing (opens in a new tab) also stops at 3M post reads per billing cycle; above that, X sells only Enterprise contracts. On Sorsa API the 100 free requests cover about 2,000 image posts, failed requests are never charged, and the plan's rate per 1,000 requests applies to every endpoint.

Time follows from the rate limit, 20 requests per second per key on every plan. 1M image posts take 50K requests, about 42 minutes when date windows run side by side; the image files then download straight from X's servers at your connection's speed.

#Get the images of specific posts

To get the images of posts you already have, send their links or IDs to POST /tweet-info-bulk (opens in a new tab): up to 100 posts per request, from $0.014 per 1,000, with the same entities list the search returns.

Terminal
curl -X POST "https://api.sorsa.io/v3/tweet-info-bulk" \
  -H "ApiKey: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"tweet_links": [
        "https://x.com/northfield_photo_lab/status/1984213377309245441",
        "1983850021164118016"]}'

A single post goes to POST /tweet-info (opens in a new tab) with tweet_link. Pass each photo's link through the same orig function and you have the files at full size. On the official X API the same lookup needs a bearer token plus expansions=attachments.media_keys and media.fields=url, and from November 22 each image in the answer adds $0.005.

Videos and GIFs come back in the same entities list under their own type. Their MP4 files and quality variants take one more step, covered in the guide to download Twitter media by API; for full text exports with a media column, see how to download all tweets from a user.

#Start your first image download

To download every image of an X account or a search at full size, use Sorsa API: about 20 image posts per request from $0.07 per 1,000, with no risk to your X account. Copy the script, add your key, and your 100 free requests download the first 2,000 image posts.

#FAQ

Is there a free Twitter image scraper?

Yes, for small jobs. The free Media Downloader saves every photo of one post at full size, and the free Export Tweets tool exports an account's latest posts with their image links. For whole accounts and searches, every new Sorsa API account gets 100 free requests: about 2,000 image posts, with no card, and the requests never expire.

Which API should I use to scrape Twitter images?

Use Sorsa API: POST /search-tweets with filter:images returns about 20 image posts per request from $0.07 per 1,000, against $10.00 per 1,000 one-image posts on the official X API from November 22, 2026. It needs one API key instead of a developer account and OAuth, searches back to 2006, runs at 20 requests per second on every plan and never asks for your X login.

Yes. Scraping public images for research or an archive is legal and common. The copyright in each image stays with whoever made it, so get the owner's permission before you publish or sell the files. Sorsa API never asks for your X login, so your X account stays out of it.

Can I download images from a private Twitter account?

No. Posts of a protected account are visible only to the followers it approves, so no API returns them, Sorsa API included, and a request for them is never charged. Tools that claim otherwise read X through the login of an account that follows it.

Does the account owner see that I downloaded their images?

No. X doesn't tell anyone when their posts or images are viewed or downloaded. With Sorsa API the requests go from Sorsa API to X, so neither X nor the account owner learns who asked, and your own X account takes no part in it.

Can I download Twitter videos and GIFs the same way?

Partly. Videos and GIFs sit in the same entities list as photos, under their own type, with a thumbnail as preview, so the search and the cursor work the same. The playable MP4 file and its quality variants take one more step, which the guide to downloading Twitter media by API walks through.

#How we checked

On November 2, 2026 we requested four public images from pbs.twimg.com in every size and format and recorded the width and file size of each answer. We also ran a public post through our free Media Downloader, which runs on Sorsa API, to see the photo link it returns. We tested both scripts' link handling and file saving against a stand-in for the API and real pbs.twimg.com files. Endpoints, fields and page sizes come from the live Sorsa API reference; the official X API's prices come from docs.x.com and X's notice of October 23, 2026, both read the same day. The API responses shown are shortened examples built from the reference.

By the Sorsa API team. We build and run Sorsa API, the read-only Twitter/X API and scraper, in production since 2022 (formerly TweetScout API) with 6B+ requests served. About Sorsa API

What changed
  • Rewritten from scratch: measured image sizes and the name=orig rule, a resumable Python and Node.js script with a file log, hashtag and search queries, a no-code route with a link converter, and costs that include X's November 22 media billing.
  • First published.