Getting Started
Resolve a brand to an advertiser, pull its ads, then follow one ad through to its creative files.
1. Get your API key
Create an account and generate a FetchLayer API key from your dashboard. There is no Google Cloud project to set up, no OAuth consent screen, and no Google Ads API developer token to apply for.
Create free account →2. Resolve the brand to an advertiser
Google organises its transparency data by advertiser, so almost everything starts with an advertiser id. Several distinct advertisers can share a brand name — a global company often registers one per market — so compare the verified country and the approximate ad count to pick the right one.
curl -X POST https://api.fetchlayer.dev/google-ad-library/search-advertisers \
-H "Authorization: Bearer ss-your-key" \
-H "Content-Type: application/json" \
-d '{"query":"Nike","region":"US","limit":10}'3. Pull that advertiser's ads
curl -X POST https://api.fetchlayer.dev/google-ad-library/advertiser-ads \
-H "Authorization: Bearer ss-your-key" \
-H "Content-Type: application/json" \
-d '{"advertiser":"AR16735076323512287233","region":"US","surfaces":["youtube","search"],"limit":50}'There is no keyword search over ad copy
/search-ads takes a brand name or a website, resolves it to advertisers, and returns their ads — it reports what it matched on matchedAdvertisers. Once you hold an advertiser id, use /advertiser-ads instead: it skips resolution entirely, so the wrong company can never be matched. 4. Follow an ad to its creative
Every ad carries an adId and an advertiserId. Google needs both to locate an ad, so pass the pair — or pass the full Ad Library ad URL, which already carries them.
curl -X POST https://api.fetchlayer.dev/google-ad-library/ad-media \
-H "Authorization: Bearer ss-your-key" \
-H "Content-Type: application/json" \
-d '{"ad":"CR01095486659304095745","advertiser":"AR16735076323512287233","kinds":["video","image"],"probe":true}'Tip
downloadUrl, not its raw url. Google's own video URLs are signed, expire, and only play back from the address they were issued to. GET /media is the one endpoint that takes query parameters instead of a JSON body, and it supports HTTP Range requests — so a video streams and seeks in a player instead of downloading whole. 5. Check the notes before you trust the totals
Every response carries a notes array. It reports what is specific to that result — a page walk that stopped short of what you asked for, a listing that may be incomplete, run dates that could not be resolved. Alongside it, approximateTotal is a { min, max } range, not an exact count: Google never publishes an exact figure. A quiet, well-formed, partial answer is the failure mode to guard against.
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.