Getting Started
Search the TikTok Ad Library in two steps, then follow an ad through to its video file.
1. Get your API key
Create an account and generate a FetchLayer API key from your dashboard. No TikTok developer account, app review, or access token is involved.
Create free account →2. Search for ads
curl -X POST https://api.fetchlayer.dev/tiktok-ad-library/search-ads \
-H "Authorization: Bearer ss-your-key" \
-H "Content-Type: application/json" \
-d '{"query":"skincare routine","country":"DE","adStatus":"active","limit":30}'The date window defaults to 30 days
shownFrom and shownTo out and the search covers the last 30 days, not all time. Set both explicitly for anything historical, and check filters in the response to see the window you actually ran. 3. Find one advertiser, then pull their rotation
Listings here are scoped by registered business name, not by TikTok handle, and one brand can be registered as several legal entities. Resolve the name first, then pass it back.
curl -X POST https://api.fetchlayer.dev/tiktok-ad-library/search-advertisers \
-H "Authorization: Bearer ss-your-key" \
-H "Content-Type: application/json" \
-d '{"query":"Samsung","limit":10}'
curl -X POST https://api.fetchlayer.dev/tiktok-ad-library/advertiser-ads \
-H "Authorization: Bearer ss-your-key" \
-H "Content-Type: application/json" \
-d '{"advertiser":"SAMSUNG ELECTRONICS (UK) LIMITED","country":"GB","adStatus":"all","pages":3}'4. Follow an ad to its video
Every ad carries an adId. Pass it to /ad-media to resolve the video, cover frame and images, then stream the bytes from /media.
curl -X POST https://api.fetchlayer.dev/tiktok-ad-library/ad-media \
-H "Authorization: Bearer ss-your-key" \
-H "Content-Type: application/json" \
-d '{"ad":"1874672994333698","kinds":["video","thumbnail"],"probe":true}'Tip
downloadUrl served through GET /media, which does not — use that one if you are storing the link rather than the bytes. It supports HTTP Range, so a video seeks in a player. 5. Read the notes
Every response carries a notes array. It reports what this result could not do — a page walk that stopped before pagesRequested, an upstream cap, a filter the library declined to honour. A thin result set with an empty notes really is thin; a thin one with notes is truncated. Compare pagesScraped with pagesRequested, and adCount with totalMatches, before you conclude a brand is quiet.
Error responses
Every error, on every route, is JSON in one shape: error (what went wrong, in words), status, code, retryable and requestId, which also comes as the X-Request-Id header. Branch on code and retryable rather than on the text; on a 5xx the text is generic. Quote requestId when reporting a problem.
{
"error": "Missing required parameter: url",
"status": 400,
"missing": ["url"],
"code": "invalid_input",
"retryable": false,
"requestId": "922874e3-c0d0-40d8-8866-d198af132420"
}| code | Status | Retryable | Meaning |
|---|---|---|---|
| invalid_input | 400 | no | The request needs fixing; error names the field. A missing required parameter also lists it in missing. |
| unauthorized | 401 | no | The API key is missing or not valid. |
| insufficient_credits | 402 | no | Nothing left to spend: no credits, a used-up free trial, or a monthly allowance and credit balance both spent. error says which. Waiting does not help; topping up does. |
| forbidden | 403 | no | This key or account is not allowed this request. |
| not_found | 404 | no | What the request names does not exist or is not public. |
| expired | 410 | no | What the request refers to is no longer available; ask for it afresh. |
| quota_exceeded | 429 | no | A request-rate limit was reached; error says which. Slow down before sending more. |
| cancelled | 499 | no | The caller closed or cancelled the request before it finished. The work was stopped and nothing was charged. |
| time_limit | 503 | yes | Your timeoutMs ran out before there was anything to return; ask again, or allow more time. |
| temporarily_unavailable | 503 | yes | Not available right now; ask again shortly. |
| store_unreachable | 503 | yes | Store routes (Shopify, WooCommerce): the store itself can’t be reached right now — it may be offline. Ask again later. Where a store route doesn’t say so yet, an offline store is temporarily_unavailable. |
| upstream_error | 502 | yes | The source answered in a way that could not be used; ask again later. |
| internal_error | 500 | yes | Something failed on our side; ask again later, and quote requestId if it persists. |
| tool_error | — | no | MCP only: a tool call failed in a way no other code describes; error says what. |
More codes may be added; treat one you do not recognise by its retryable flag. Over MCP, a tool error carries the same fields in structuredContent, and as a JSON text block beside the message.