gallery-dl for Twitter (X): Config, Cookies, Rate Limits and Fixes

By TwitterAPI.io Team•16 min read
Diagram of two routes to the same X media file. Top, orange: gallery-dl reads browser cookies with --cookies-from-browser, calls X's web GraphQL endpoints such as UserMedia and Likes, parses them in extractor/twitter.py and downloads the original photo or mp4; it breaks when X changes a response shape, waits on 429s and runs as your account. Bottom, blue: an X-API-Key request to /twitter/user/last_tweets returns extendedEntities.media[], from which the script picks the highest-bitrate mp4; pay per post returned, $0.15 per 1,000. Footer: our study read 860 posts from 8 accounts in 45 requests, collected in 2026.
gallery-dl drives X's web endpoints with your session; the API route reads the same media objects with a key. Diagram; the footer numbers come from the study on this page.
TL;DR
  • Cookies, not passwords. X login with username and password was removed in gallery-dl v1.30.10; use --cookies-from-browser or a cookies.txt file.
  • "Login rejected as suspicious" came from the old password flow; the fix is the same: upgrade and pass browser cookies.
  • Rate limits: "ratelimit": "wait" (default) sleeps until X's reset header; "abort:N" stops after N waits.
  • Not working? Upgrade first: the October 2026 KeyError: 'legacy' was fixed in v1.32.18; report with --verbose --write-pages.
  • What you will download: in 860 posts from 8 organization accounts, 53.8% of own posts carried media and every one of 225 videos listed 5 variants (median), best mp4 10.37 Mbps.
  • API route: /twitter/user/last_tweets returns the same media objects with an API key; $0.15 per 1,000 posts versus $5 per 1,000 on X's pay-per-use rate card.

gallery-dl is a GPL-2.0 command-line downloader for image and video galleries on many sites, and its X (Twitter) extractor is the one people search for most often: profile media timelines, likes, bookmarks, lists, searches and single posts. It works by calling the same web endpoints x.com's own pages call, with your browser session. That design is why it keeps working without an official API key, and also why it breaks a few times a year when X changes the shape of a response.

This page is the developer reference we wanted when we set it up: the options that matter for X, taken from gallery-dl's own docs/configuration.rst; the errors people actually hit, checked against the project's GitHub issues and release notes; what a downloader really fetches from a timeline, measured on 860 posts; and the API route for the same job when you need it to keep running. For saving a few photos by hand, the consumer walkthrough is in our guide to downloading pictures from Twitter.

One boundary up front: gallery-dl runs as your X account whenever you give it cookies. Keep its pace human, respect X's limits, and download only media you have the right to keep.

Own posts carrying media in 8 organization timelines

53.8% of 701 own posts (collected in 2026); every video listed a median of 5 variants and its best mp4 ran at a median 10.37 Mbps. Reading all 860 posts cost $0.129.

01 — Section

gallery-dl is on PyPI as gallery-dl (version 1.32.19 at the time of writing, Python 3.8 or newer, GPL-2.0-only license). The project also publishes standalone executables, and its README lists optional extras; the one that matters for X on Linux is SecretStorage, which --cookies-from-browser uses to read GNOME keyring passwords for decrypting Chromium cookies.

Sign in to x.com in a regular browser first. gallery-dl then reads that session: --cookies-from-browser firefox (Chromium-based browsers, Firefox and Safari are supported per the README) or --cookies <path> for a Netscape-format cookies.txt exported by a browser extension. Without cookies the extractor falls back to a guest token, which X answers for some single posts and almost nothing else; most timelines come back as "authenticated cookies" required.

URLs it understands. The supported-sites table lists avatars, backgrounds, bookmarks, communities, followers, followed users, hashtags, highlights, home feed, likes, lists, list members, media timelines, notifications, quotes, search results, timelines, single Tweets and user profiles. https://x.com/<user> uses the include option (default timeline); https://x.com/<user>/media reads only the Media tab.

Keep it current. The X extractor changes almost weekly: the releases from late September to late October 2026 (v1.32.14, v1.32.15, v1.32.17 and v1.32.18) each carry X fixes. Most "gallery-dl twitter not working" reports end with "upgrade".

bash
# install or upgrade (PyPI; Python 3.8+)
python -m pip install -U gallery-dl
gallery-dl --version

# one public profile's media timeline, cookies read from your logged-in Firefox profile
gallery-dl --cookies-from-browser firefox "https://x.com/NASA/media"

# same job with a Netscape-format cookies.txt exported from your browser
gallery-dl --cookies "$HOME/cookies-x.txt" "https://x.com/NASA/media"

# your own likes and bookmarks (need your session cookies)
gallery-dl --cookies-from-browser firefox "https://x.com/YOUR_HANDLE/likes"
gallery-dl --cookies-from-browser firefox "https://x.com/i/bookmarks"

# resume an interrupted timeline from the cursor gallery-dl printed
gallery-dl -o cursor=PASTE_CURSOR_HERE "https://x.com/NASA/media"

# when something breaks, this is what the maintainer asks for
gallery-dl --verbose --write-pages "https://x.com/NASA/status/1234567890"
02 — Section

A working config.json for the twitter extractor

gallery-dl reads config.json from its standard locations (for example ~/.config/gallery-dl/config.json on Linux and macOS, %APPDATA%\gallery-dl\config.json on Windows). Everything for X sits under extractor.twitter. The block below is a sensible starting point for archiving one account's own media; each option and its default is described in docs/configuration.rst.

OptionDefaultWhat it controls
cookiesnoneBrowser name list such as ["firefox"], a cookies.txt path, or name-value pairs
include"timeline"Which profile sections to walk: media, tweets, with-replies, likes, highlights, avatar, background, info, or all
retweetsfalseFetch media from reposts; "original" takes metadata from the original post
repliestrueMedia from replies; "self" keeps only replies to the same user
quotedfalseAlso fetch the quoted (original) post
videostrueDownload videos; "ytdl" hands them to yt-dlp; false skips video posts
size["orig", "4096x4096", "large", "medium", "small"]Image version, with the rest as fallbacks
ratelimit"wait""wait", "wait:N" seconds, "abort" or "abort:N"
locked"abort"What to do on "account is temporarily locked": abort, or wait for you
cardsfalseDownload images from link cards (true) or card video via yt-dlp ("ytdl")
unavailablefalseTry media X marks Unavailable, such as geoblocked videos
text-tweetsfalseEmit metadata for text-only posts (needs a metadata post processor)

Two options worth knowing because of our study. cards defaults to false, and in our sample 42.1% of organization posts were link cards whose image lives in the card, not in the post's media list; if you expect a news account's thumbnails, you will not get them without "cards": true. And archive (a general extractor.* option) stores the IDs of downloaded files in a SQLite file so reruns skip them, which keeps a nightly sync short.

Searchers asking about "gallery dl twitter text" usually want the post text next to each file. That is the --write-metadata command-line flag (a JSON file per download) or the metadata post processor; text-tweets only adds posts that have no media.

json
{
  "extractor": {
    "twitter": {
      "cookies": ["firefox"],
      "include": ["media"],
      "retweets": false,
      "replies": "self",
      "quoted": false,
      "videos": true,
      "size": ["orig", "4096x4096", "large", "medium", "small"],
      "ratelimit": "wait",
      "locked": "abort",
      "unavailable": false,
      "cards": false,
      "text-tweets": false,
      "archive": "~/gallery-dl/archive-twitter.sqlite3"
    }
  }
}
03 — Section

Cookies, likes, bookmarks and the "login rejected as suspicious" error

The most searched gallery-dl X error is AuthenticationError: Login rejected as suspicious. The project's GitHub tracker holds a run of reports with that title from 2024 and 2025 (issues 6218, 6288, 6822, 7042 and 7318 among them). It was X refusing the scripted username-and-password sign-in that older gallery-dl versions performed.

gallery-dl stopped trying. Commit 1ea09058c7, "[twitter] remove login support", shipped in v1.30.10 on October 12, 2025. In the current extractor/twitter.py, the login routine only logs an error: "Login with username & password is no longer supported. Use browser cookies instead." The username and password options in configuration.rst no longer list twitter among the sites that support them. So the fix for the suspicious-login error is not a different password setting; it is to delete username/password from your twitter block, upgrade, and pass cookies.

Which cookies. gallery-dl checks for X's auth_token session cookie and uses the ct0 cookie as the CSRF token (option csrf, default "cookies"). --cookies-from-browser reads them from the browser profile you name; the config form is "cookies": ["firefox"], with optional profile, keyring, Firefox container and domain entries, for example ["chromium", "Private", "kwallet", null, ".twitter.com"] from the docs. With a cookies.txt file, cookies-update (default true) writes refreshed session cookies back to it.

When cookies expire. Signing out of x.com in the browser, or X ending the session, invalidates the cookies. The symptom is the same "authenticated cookies" requirement on every timeline. Sign in again in the browser and rerun; nothing in gallery-dl needs resetting.

What we will not cover. Some guides suggest rotating several accounts' cookies (cookies-select) to spread requests. That multiplies the number of accounts acting on X's limits, which X's rules do not allow, and we do not recommend it. If one session is not enough throughput, the job has outgrown a personal CLI.

Two of the most searched gallery-dl X jobs are personal archives: everything you have liked, and everything you have bookmarked. Both are handled by dedicated extractors in extractor/twitter.py: TwitterLikesExtractor matches https://x.com/<user>/likes, and TwitterBookmarkExtractor matches https://x.com/i/bookmarks. Both read through your session, so both need your cookies. Bookmarked items get a date_bookmarked field in their metadata; likes get user_likes.

Useful details. Since v1.32.17, "replies": "self" works for likes and bookmarks, and /i/history/likes URLs are accepted. Bookmark folders on Premium accounts are an open request on Codeberg at the time of writing, so plan on the main bookmarks list only. include: ["likes"] on a profile URL is the config form of the likes job.

Media from link cards. On news-style accounts, much of what looks like an image is a link card. gallery-dl skips cards by default (cards: false); set true to download images from supported cards, or "ytdl" to hand unsupported card video to yt-dlp. cards-blacklist excludes card types you do not want.

On the API route. Our API returns the timeline, search and single-post media covered below with an API key and no X account. Bookmarks are available through /twitter/bookmarks_v2, which acts on a logged-in session: one /twitter/user_login_v2 login ($0.10, with a proxy you supply) returns login_cookies that you pass along. That call acts as your account, so X can rate-limit, lock or suspend it; keep a human pace, and expect the session to need a fresh login from time to time. There is no endpoint for another user's likes list.

04 — Section

Rate limits, account locks and the 429 wait

X limits how often a session can call each web endpoint. gallery-dl reads the x-rate-limit-remaining header on every response and, when it gets low or X answers 429, calls its _handle_ratelimit routine. That routine follows the ratelimit option: with the default "wait" it sleeps until the time in x-rate-limit-reset (or 60 seconds if X sent none). That is why a big likes download can sit silent for many minutes: it is waiting, not hung.

SettingBehavior on hitting the limit
"wait" (default)Sleep until X's reset time, then continue
"wait:N"Sleep N seconds, then continue
"abort"Raise "Rate limit exceeded" and stop
"abort:N"Wait up to N times, then stop

Account locks. If X answers "this account is temporarily locked", gallery-dl raises AuthorizationError by default (locked: "abort"); with "wait" it pauses until you press Enter after unlocking the account in a browser. A lock is X telling you the session looked automated; slow down rather than retry.

Resuming. When a timeline run is interrupted, gallery-dl logs a line like "Use '-o cursor=...' to continue downloading from the current position" (option cursor, default true). Pass that value back with -o cursor= to resume. A cursor from one timeline cannot be used on another.

Pace. gallery-dl's general sleep-request option adds a delay between requests if you want one. For X, keep the defaults, run one job at a time per account, and schedule large archives across days instead of forcing them through. retries-api (default 9) controls retries on server errors.

05 — Section

The latest round of reports came in October 2026. GitHub issue 9822, "Twitter broken for url; KeyError: 'legacy'", opened October 19, 2026 on version 1.32.16, showed KeyError: 'legacy' in _tweets_single. The page the reporter saved with --write-pages held a TweetTombstone instead of a post. The maintainer fixed it the same day (commit 6dead76, "fix KeyError: 'legacy' for quote Tweet chains"), and a second commit the next day handled a related KeyError: 'result' on deleted quoted posts. Both fixes are in v1.32.18, released October 25, 2026, and the issue is closed as completed. A separate change in v1.32.18 fixed the search-limit option.

We found claims that the same month's change to X's GraphQL user object (user records without the old legacy block) broke gallery-dl. The issue thread does not show that: its traceback and saved page point to tombstoned and deleted quoted posts, not the user object. Other community scrapers did hit KeyError failures on user fields around the same time, which is covered on our twscrape page.

ReleaseDateX (twitter) changes in the notes
v1.32.14Sep 27, 2026fix limitedActionResults quotes; new quoted-expand option
v1.32.15Oct 3, 2026warn on login page redirect
v1.32.17Oct 17, 2026fix "replies": "self" for likes and bookmarks; support /i/history/likes URLs
v1.32.18Oct 25, 2026fix KeyError: 'legacy' for quote chains; fix deleted quoted Tweets; fix search-limit
v1.32.19Oct 31, 2026no X entries

Where to report now. Release notes link fixes to both GitHub and Codeberg issue numbers, gallery-dl's own "unexpected error" message now points users to codeberg.org/mikf/gallery-dl/issues, and the README's dev install pulls from Codeberg. Open X issues there at the time of writing include an age-restricted-user NotFoundError, bookmark folders on Premium accounts, and a 404 at the end of a user's media timeline.

A troubleshooting order that works. Upgrade (pip install -U gallery-dl). Confirm the cookies are fresh by opening x.com in that same browser profile. Rerun one failing URL with --verbose --write-pages and read the last API error line. Search the tracker for the exception name. Only then change config.

06 — Section

What a downloader actually fetches: media in 860 organization posts

To give the configuration choices above real numbers, we read the latest posts of eight public organization timelines (@NASA, @NatGeo, @PlayStation, @Xbox, @SpaceX, @Nintendo, @Reuters and @espn) and counted what a media downloader would see in each post's extendedEntities.media[]. Of 860 posts, 159 were reposts (excluded, like gallery-dl's default retweets: false), leaving 701 of the accounts' own posts.

53.8% of own posts carried at least one media item, 458 items in all: 231 photos (50.4%), 225 videos (49.1%) and 2 GIFs. Most media posts had one item; 10.6% had two to four. The spread between accounts is the real lesson: @PlayStation put media in 97.1% of its posts, while @Nintendo had media in one post of 118 and @Reuters in 18.6%. Those two accounts post link cards: 116 and 95 card posts with no media item, 42.1% across the sample. gallery-dl with default settings downloads nothing from those posts.

AccountOwn postsPosts with mediaPhoto / video / GIFCard-only posts
@PlayStation7097.1%25 / 49 / 00
@espn9284.8%35 / 43 / 014
@Xbox2382.6%5 / 14 / 01
@NASA6876.5%44 / 12 / 016
@NatGeo10065.0%21 / 45 / 035
@SpaceX11264.3%94 / 48 / 018
@Reuters11818.6%6 / 14 / 295
@Nintendo1180.8%1 / 0 / 0116

Photos in this sample were large: the median photo's longest side in original_info was 2,048 px, 52.8% were 2,048 px or more, and the largest was 8,000 px. That is what gallery-dl's default size: ["orig", ...] buys you over the large copy; our picture-download guide measured a 3858x2170 photo coming back at 2048x1152 as large and at full size only as orig.

Horizontal stacked bars for eight organization accounts showing the share of own posts with media, link cards only, and text only. PlayStation 97% media, espn 85%, Xbox 83%, NASA 76%, NatGeo 65%, SpaceX 64%, Reuters 19% with most of the rest link cards, Nintendo under 1% media and almost all link cards. Overall 53.8% of 701 own posts carry media and 42.1% are link cards; 231 photos, 225 videos, 2 GIFs.
Share of each account's own posts that carry media, are link cards, or are text only. 701 own posts from 8 organization timelines, collected in 2026.
07 — Section

Videos: variants, bitrates and the HLS playlist

Every one of the 225 videos in the sample listed several files in video_info.variants[]: a median of 5 variants, between 4 and 6. 100% of videos included an HLS playlist (application/x-mpegURL, a .m3u8 URL), and the rest were progressive mp4 renditions at different resolutions, up to 5 per video. Each mp4 variant carries a bitrate; the playlist does not.

Downloaders pick the highest-bitrate mp4. In our sample that best file had a median bitrate of 10.37 Mbps, the 1080p tier (126 of 225 videos). 42 videos, mostly SpaceX launch footage, offered a 2160p file at 25.13 Mbps, and 84.0% of videos topped out at 1080p or higher. The lowest best-file bitrate was 1.28 Mbps. Video length ran from seconds to 292 seconds, median 29.3 s.

Best mp4 bitrateVideosTypical top resolution
25.13 Mbps422160p
10.37 Mbps1261080p
8.77 Mbps221080p
2.18 Mbps26720p
1.28 Mbps9720p

What this means for disk. A 30-second video at 10.37 Mbps is about 39 MB; at 25.13 Mbps it is about 94 MB. The two GIFs in the sample came as a single mp4 variant with bitrate 0, which is how X stores animated GIFs; gallery-dl saves them as mp4. gallery-dl's previews option additionally downloads video previews, and videos: false skips video posts entirely.

The snippet below is the selection rule in a few lines of Python. It is what gallery-dl does by default, and what our video downloader tool does for single links.

python
import requests

BASE = "https://api.twitterapi.io"
HEADERS = {"X-API-Key": "YOUR_API_KEY"}

def best_file(media):
    """Return the URL gallery-dl would pick by default for one media object."""
    if media["type"] == "photo":
        return media["media_url_https"] + "?name=orig"
    variants = (media.get("video_info") or {}).get("variants") or []
    mp4 = [v for v in variants if v.get("content_type") == "video/mp4"]
    if not mp4:  # only an HLS playlist (.m3u8) is listed
        return None
    return max(mp4, key=lambda v: v.get("bitrate") or 0)["url"]

r = requests.get(f"{BASE}/twitter/user/last_tweets",
                 headers=HEADERS, params={"userName": "NASA"}, timeout=30)
r.raise_for_status()
for tweet in r.json()["data"]["tweets"]:
    for m in (tweet.get("extendedEntities") or {}).get("media") or []:
        print(tweet["id"], m["type"], best_file(m))
Two bar charts. Left: of 225 videos, 35 listed 4 variants, 148 listed 5 and 42 listed 6; median 5. Right: best mp4 bitrate per video, 126 at 10.37 Mbps (highlighted, the median), 42 at 25.13 Mbps, 22 at 8.77 Mbps, 26 at 2.18 Mbps and 9 at 1.28 Mbps. Title: every video ships an HLS playlist (100%); 84% top out at 1080p or higher.
Variants per video and the highest mp4 bitrate offered, 225 videos from 8 organization timelines, collected in 2026.
08 — Section

When the CLI is not enough: the API route and what it costs

gallery-dl is the right tool for a personal archive you run by hand. It is a poor base for a product: it needs a logged-in X session, it waits out rate limits on that session's schedule, and it stops working whenever X changes a response until a new release ships. If downloads feed a service, a scheduled pipeline or a customer, use an API that returns the same media objects with an API key and no X account.

The mapping. A profile's media timeline is /twitter/user/last_tweets (or /twitter/user/tweet_timeline by user ID), filtering posts whose extendedEntities.media[] is not empty. A single post or a batch of posts is /twitter/tweets with tweet_ids. A search like gallery-dl's search URLs is /twitter/tweet/advanced_search with filter:media in the query. Media objects carry the same fields gallery-dl reads: type, media_url_https, original_info, and video_info.variants[].

Jobgallery-dlAPI route
One account's mediahttps://x.com/<user>/media + cookies/twitter/user/last_tweets?userName=<user>
One posthttps://x.com/<user>/status/<id>/twitter/tweets?tweet_ids=<id>
Search resultshttps://x.com/search?q=... + cookies/twitter/tweet/advanced_search?query=...
Your bookmarkshttps://x.com/i/bookmarks + cookies/twitter/bookmarks_v2 with login_cookies

Cost. Tweet endpoints are billed per post returned: 15 credits, which is $0.15 per 1,000 posts, and a full 20-post page costs $0.003. The whole study on this page, 860 posts in 45 requests, cost $0.129. On X's official pay-per-use rate card (docs.x.com/x-api/getting-started/pricing), a post read is $0.005, so the same 860 posts would be $4.30, and 1,000 posts are $5.00 against $0.15, about 33 times more. X's pay-per-use plan is capped at 3 million post reads a month, with Enterprise above that. Downloading the media files themselves from pbs.twimg.com and video.twimg.com is not billed by either.

Rate limits on our side are per API key: 0.2 requests per second before the first payment, 20 once paid, 50 on Scale, 500 on Business. New accounts that pass the automatic bot check get up to $1 of free credits (at most 100,000 credits, no card, one per person or network, expiring after 31 days), enough to read more than 6,000 posts. Plans and credit packs are on the pricing page.

The end-to-end Python script at the bottom of this page pages through an account with has_next_page and next_cursor, skips reposts like gallery-dl's default, saves photos at ?name=orig and videos at the highest mp4 bitrate, and skips files it already has. For concurrency, retries and HLS-only videos at scale, see our bulk media download guide.

09 — Section

How we collected this

Question. What does a media downloader such as gallery-dl actually fetch from typical public organization timelines on X: how many posts carry media, which types, how many variants per video, and at what bitrate?

Accounts and method. Eight public organization accounts: @NASA, @NatGeo, @PlayStation, @Xbox, @SpaceX, @Nintendo, @Reuters and @espn. We read each account's latest posts with /twitter/user/last_tweets (replies excluded, newest first), up to six pages of 20, through a rate-limited read-only client. 45 requests returned 860 posts; 159 reposts were set aside, leaving 701 own posts. Media fields were read from extendedEntities.media[]; link cards from the card field. Collected in 2026.

What the sample cannot show. Eight organization accounts are not a random sample of X: brand and media accounts post more video and higher-bitrate files than most people. Each account's window is its latest posts, so a launch week or a campaign can shift the mix. We did not run gallery-dl against these accounts, and we did not measure X's rate limits on a logged-in session; the gallery-dl behavior described on this page comes from its source code, docs, issues and release notes.

Privacy. Aggregates only, organization accounts only, no analysis of reposted or quoted authors. The analysis script, protocol and results are kept with the study files.

python
"""Download every photo and best-quality video from one public X account.
The API-route equivalent of `gallery-dl https://x.com/<user>/media`.
pip install requests ; set API_KEY ; python x_media_dl.py NASA 200
"""
import os
import sys
import time

import requests

API_KEY = os.environ.get("TWITTERAPI_IO_KEY", "YOUR_API_KEY")
BASE = "https://api.twitterapi.io"
HEADERS = {"X-API-Key": API_KEY}


def timeline(user, max_posts):
    """Yield the account's own posts, newest first, 20 per page."""
    cursor, seen = "", 0
    while seen < max_posts:
        params = {"userName": user}
        if cursor:
            params["cursor"] = cursor
        r = requests.get(f"{BASE}/twitter/user/last_tweets", headers=HEADERS,
                         params=params, timeout=30)
        if r.status_code == 429:  # our own per-key QPS limit
            time.sleep(5)
            continue
        r.raise_for_status()
        body = r.json()
        if body.get("status") == "error":
            raise SystemExit(body.get("msg"))
        tweets = (body.get("data") or {}).get("tweets") or []
        for t in tweets:
            if t.get("retweeted_tweet"):  # like gallery-dl's default "retweets": false
                continue
            seen += 1
            yield t
        if not body.get("has_next_page") or not tweets:
            return
        cursor = body.get("next_cursor") or ""


def best_url(media):
    if media["type"] == "photo":
        return media["media_url_https"] + "?name=orig", "jpg"
    variants = (media.get("video_info") or {}).get("variants") or []
    mp4 = [v for v in variants if v.get("content_type") == "video/mp4"]
    if not mp4:
        return None, None  # HLS-only: hand the .m3u8 to ffmpeg instead
    return max(mp4, key=lambda v: v.get("bitrate") or 0)["url"], "mp4"


def main(user, max_posts):
    out = os.path.join("downloads", user)
    os.makedirs(out, exist_ok=True)
    files = 0
    for t in timeline(user, max_posts):
        media = (t.get("extendedEntities") or {}).get("media") or []
        for n, m in enumerate(media, 1):
            url, ext = best_url(m)
            if not url:
                continue
            path = os.path.join(out, f"{t['id']}_{n}.{ext}")
            if os.path.exists(path):  # cheap archive: skip what we already have
                continue
            with requests.get(url, stream=True, timeout=60) as resp:
                resp.raise_for_status()
                with open(path, "wb") as fh:
                    for chunk in resp.iter_content(1 << 16):
                        fh.write(chunk)
            files += 1
            print(m["type"], path)
    print(f"done: {files} new files")


if __name__ == "__main__":
    main(sys.argv[1] if len(sys.argv) > 1 else "NASA",
         int(sys.argv[2]) if len(sys.argv) > 2 else 200)

Researching this?Free starter credits cover end-to-end testing — Google sign-in, no card. Get a key.

10 — Questions

Questions readers ask

How do I fix "login rejected as suspicious" in gallery-dl for Twitter?

Remove username and password from your twitter config, upgrade gallery-dl, and pass browser cookies instead (--cookies-from-browser firefox or "cookies": ["firefox"]). The error came from X refusing the scripted password sign-in; gallery-dl removed X password login entirely in v1.30.10 (October 2025).

How do I give gallery-dl my Twitter cookies?

Sign in to x.com in a browser, then either run gallery-dl --cookies-from-browser <browser> URL (Chromium-based browsers, Firefox and Safari), or export a Netscape-format cookies.txt with a browser extension and pass --cookies path/to/cookies.txt. In config.json the same goes under extractor.twitter.cookies. gallery-dl needs the auth_token session cookie and uses ct0 for the CSRF token.

Why does gallery-dl stop for minutes on Twitter?

It hit X's rate limit and is waiting. The default ratelimit: "wait" sleeps until the reset time X sends in x-rate-limit-reset. Use "abort" or "abort:N" to stop instead, and resume later with the -o cursor= value gallery-dl prints.

Can gallery-dl download my Twitter likes and bookmarks?

Yes, with your cookies: https://x.com/<your handle>/likes and https://x.com/i/bookmarks. Since v1.32.17, /i/history/likes URLs work and "replies": "self" applies to likes and bookmarks. Bookmark folders on Premium accounts are an open request at the time of writing.

gallery-dl Twitter is not working. What should I check first?

Upgrade with pip install -U gallery-dl, because X fixes ship almost weekly; the October 2026 KeyError: 'legacy' was fixed in v1.32.18. Then refresh your browser session, rerun one URL with --verbose --write-pages, and search the tracker for the exception name.

Why does gallery-dl download nothing from some accounts?

Their posts may be link cards, not media. In our sample, 42.1% of organization posts were card-only, and @Nintendo had media in one of 118 posts. Set "cards": true to download card images. Protected posts raise an "authenticated cookies" error without a session that can see them.

Does gallery-dl download the highest quality Twitter video?

By default it saves the highest-bitrate mp4 X lists. In our study of 225 videos, that best file had a median bitrate of 10.37 Mbps (1080p), and 42 videos offered 25.13 Mbps (2160p). "videos": "ytdl" hands videos to yt-dlp if you prefer its format selection.

Is there an API alternative to gallery-dl for Twitter media?

Yes. /twitter/user/last_tweets, /twitter/tweets and /twitter/tweet/advanced_search return the same extendedEntities.media[] objects with an API key and no X account, at $0.15 per 1,000 posts, versus $0.005 per post read ($5 per 1,000) on X's official pay-per-use rate card.

11 — Further reading

Continue

Sources & further reading
More from this series

Writing about something you built with TwitterAPI.io? Our community rewards program pays API credits for honest tutorials, build logs and videos.

Build it

Stop reading. Start building.

Starter credits cover real testing on real data. Google sign-in, no card, no application queue.

Get an API key

Already building with TwitterAPI.io? Publish an honest tutorial or build log about it and earn up to $200 a month in API credits.