What the TikTok Ad Library scraper collects
Extracts advertisements from TikTok's EU Ad Library by region or advertiser keyword: the advertiser, video and image creatives, first and last shown dates, TikTok's audience bucket and the policy reason on rejected ads. A browser opens the library once to mint its request header, then pages through the search endpoint by cursor and writes JSON or CSV.
Ads by region
Input: One or more region codes (--region), a look-back window (--days)One row per ad per region: advertiser, creative type and media URLs, run dates, days shown, estimated audience and audit status.
Ads by keyword
Input: A region and a search keyword (--query)The same rows, filtered by the library's own keyword search — which matches more than the advertiser's name, so resellers appear too.
From zero to TikTok Ad Library data in 3 steps
Install
Clone the repository and install one browser engine. Python 3.9+.
$ git clone https://github.com/2scraper/tiktok-ads-scraper && cd tiktok-ads-scraper $ python3 -m venv venv $ ./venv/bin/pip install -r requirements.txt -r requirements-playwright.txt $ ./venv/bin/python -m playwright install chromium $ ./venv/bin/python playwright_scraper.py --region DE
Run a scraper
Pick what you need and run it. Every flag is documented in the README.
$ ./venv/bin/python playwright_scraper.py --region DE,FR,GB,IT --pages 10 --days 90 --format both$ ./venv/bin/python playwright_scraper.py --region DE --query nikeGet structured data
Rows land in JSON or CSV (
--format), with a.meta.jsonsidecar recording status, pages fetched and why the run stopped.
TikTok Ad Library data fields
26 columns per row in a predictable schema. The most useful ones:
| Field | What it holds |
|---|---|
ad_id | TikTok's ad id — not unique across regions |
row_key | {region}:{ad_id}, the unique key for dedupe and diffs |
advertiser_name | Advertiser; a few ads are published anonymously |
region | Region the ad was found in |
first_shown_at | When the ad first ran (ISO, converted from milliseconds) |
last_shown_at | When the ad last ran |
days_shown | Days between first and last shown |
creative_type | video or image, derived from the creatives |
video_count | Number of video creatives |
video_urls | The library's proxied video URLs |
cover_urls | Video cover image URLs |
image_urls | Image creative URLs |
estimated_audience | TikTok's audience bucket, e.g. "0-1K", kept as a string |
rejection_info | TikTok's policy reason on a rejected ad |
audit_status | TikTok's own audit value, passed through unmapped |
[
{
"source": "library.tiktok.com",
"scraped_at": "2026-09-22T22:26:45Z",
"url": "https://library.tiktok.com/ads/detail/?ad_id=1877067575828514®ion=DE",
"sku": "1877067575828514",
"title": "Faisal bin Abdullah",
"ad_id": "1877067575828514",
"row_key": "DE:1877067575828514",
"advertiser_name": "Faisal bin Abdullah",
"region": "DE",
"first_shown_at": "2026-09-22T21:33:56+00:00",
"last_shown_at": "2026-09-22T21:33:56+00:00",
"days_shown": 0,
"creative_type": "video",
"video_count": 1,
"image_count": null,
"video_urls": [
"https://library.tiktok.com/api/v1/cdn/1790116006/video/aHR0cHM6Ly92MTZtLnRpa3Rva2Nkbi5j…"
],
"cover_urls": [
"https://p16-common-sign.tiktokcdn.com/tos-alisg-p-0037/oYhIw9WIyuvhAc1itiUmCXilyB7jf4AH…"
],
"image_urls": null,
"audit_status": "1",
"sor_audit_status": "1",
"show_mode": 1,
"ad_type": "2",
"rejection_info": null,
"estimated_audience": "0-1K",
"page": 1,
"position": 1
},
{
"source": "library.tiktok.com",
"scraped_at": "2026-09-22T22:26:45Z",
"url": "https://library.tiktok.com/ads/detail/?ad_id=1877067300684802®ion=DE",
"sku": "1877067300684802",
"title": "لاتفوتكم فعالياتهم😍 @الهرم #الهرم #القصيم #بريده ad",
"ad_id": "1877067300684802",
"row_key": "DE:1877067300684802",
"advertiser_name": "n.b.alqassim",
"region": "DE",
"first_shown_at": "2026-09-22T21:07:26+00:00",
"last_shown_at": "2026-09-22T21:07:26+00:00",
"days_shown": 0,
"creative_type": "video",
"video_count": 1,
"image_count": null,
"video_urls": [
"https://library.tiktok.com/api/v1/cdn/1790116006/video/aHR0cHM6Ly92MTZtLnRpa3Rva2Nkbi5j…"
],
"cover_urls": [
"https://p16-common-sign.tiktokcdn.com/tos-alisg-p-0037/ooiQOrz1WAqoAgrEixyXiI6AuQyfICc4…"
],
"image_urls": null,
"audit_status": "1",
"sor_audit_status": "1",
"show_mode": 1,
"ad_type": "2",
"rejection_info": null,
"estimated_audience": "0-1K",
"page": 1,
"position": 2
}
]First 2 of 21 rows from a real run — sample_output.json. Long values shortened here.
Handles what breaks naive TikTok Ad Library scrapers
Pages by cursor, not the decoy offset
The request's offset does nothing — offsets 0, 12 and 24 returned the same twelve ads. The scraper follows the search_id cursor and ends a region when the cursor stops advancing, whatever has_more claims.
One ad, many regions, distinct rows
An EU ad buy runs in several member states and the library lists it under each. Every row carries row_key ({region}:{ad_id}), so a multi-region run no longer collapses one country's ads into another's.
Millisecond dates read correctly
TikTok publishes run dates in milliseconds; read as seconds they land in the year 58,700. first_shown_at and last_shown_at are converted properly.
A stale token is not a block
An HTTP 421 mid-run means the library's header went stale. The run re-primes the browser to mint a fresh one instead of spending a proxy rotation on the wrong problem.
Scraping infrastructure included
The same tested core runs every 2scraper repository, so the hard parts work the same way on every site.
CAPTCHA handling
Detects the challenge a page really serves and, with a 2Captcha key, solves it only when it blocks the data — a capped number of solves per page.
Proxy rotation
Single proxy or a pool from a file, rotated on blocks with a fresh browser per exit. Credentials stay out of argv and logs.
Fingerprints
Optional complete device identity from the 2Captcha Fingerprint API — user agent, client hints, locale and timezone that agree with each other.
Honest run metadata
Every run writes a .meta.json sidecar and distinct exit codes: complete, blocked, empty and partial are never confused, and a failed run never overwrites good data.
Diff two runs
diff_runs.py compares two complete TikTok Ad Library snapshots by id — new, removed and changed rows for monitoring.
What teams do with TikTok Ad Library data
Competitor ad monitoring
Track which creatives a brand runs on TikTok, where, and for how long, with --query and diffs between runs.
Creative research
Collect video and image creatives by region to study formats and messaging that advertisers use.
Ad policy research
Find rejected ads and TikTok's stated policy reason in rejection_info.
Cross-market campaign mapping
Compare which ads run in which of the 33 regions to see how campaigns are rolled out across Europe.
Free to run. Pay only for what the site requires.
Runs with no key, proxy or account
The library's search endpoint was served to a bare datacentre address with no credentials, measured Sep 22 2026, and the daily canary runs from a GitHub runner with no secrets. A browser is required because one request header is minted by the library's own JavaScript. The repo states it needs none of 2Captcha's paid products; proxies and the Scraping Browser API are supported for volume or a chosen exit.
Optional 2Captcha services — captcha solving, the Scraping Browser API, residential proxies and fingerprints — work behind one key.
Good to know
- Only the 33 regions the EU Ad Library covers; the rest of the world is not in it.
- Twelve ads per request, so large regions can only be sampled.
- This repo does not implement a Scraper API path: a fetched URL cannot mint the header the endpoint needs.
2scraper vs. hosted scraper APIs
| 2scraper | Typical hosted API | |
|---|---|---|
| Code | Open source, MIT — read and patch it | Closed |
| Cost | Free; optional services billed only when used | Subscription or per request / per result |
| Runs on | Your machine, server or CI | The vendor's infrastructure |
| Output | JSON/CSV files + run metadata | API responses |
| When the site changes | Fix it yourself or open an issue / PR | Wait for the vendor |
TikTok Ad Library scraper FAQ
Do I need a key, proxy or TikTok account?
No. The library's search endpoint is served to a datacentre address with no credentials. Only a header minted by its own JavaScript gates it, which is why a browser is used.
Which regions are covered?
33: the EU and EEA states plus CH, GB and TR. --region US is refused with that reason, since the library does not cover it.
How many ads can I get?
Twelve per request; limit does not change that. The library reported over 17 million ads for Germany alone, so any run is a sample, and the sidecar records sample_share_pct.
Why are impressions and spend missing?
TikTok publishes both fields but they were empty on all 216 ads measured across six regions, so they are not columns.
Does --query filter by advertiser?
Since v0.1.2 it uses the library's own keyword search, which matches more than the advertiser's name, so reseller ads appear too.
Is it legal?
It reads TikTok's public advertising transparency library. Follow TikTok's terms and the law where you operate.