The Google Ads Transparency Center shows every ad an advertiser runs. There is no public API for it, but the site itself talks to an internal RPC endpoint, and that endpoint answers anonymous requests.
I spent a day mapping it. Posting the field map here because I could not find it documented anywhere.
The endpoint
POST https://adstransparency.google.com/anji/_/rpc/SearchService/SearchCreatives?authuser=
Content-Type: application/x-www-form-urlencoded
f.req=<url-encoded JSON>
The payload is protobuf-JSON, so every field is a number rather than a name:
{
"2": 40,
"3": {
"4": 3,
"8": [2250],
"12": { "1": "nike.com", "2": true },
"13": { "1": ["AR01614014350098432001"] },
"14": [5]
},
"7": { "1": 1, "2": 0, "3": 2250 }
}
| Field | Meaning |
|---|---|
2 |
page size (40 works) |
3.4 |
ad format: 1 = IMAGE, 2 = TEXT, 3 = VIDEO |
3.8 |
array of region codes |
3.12.1 |
free-text query: brand name or domain |
3.13.1 |
array of advertiser IDs |
3.14 |
array of platform codes |
4 |
pagination cursor |
The response is {"1": [creatives...], "2": nextCursor}. Feed field 2 back as request field 4 to page.
Region codes are just ISO numeric + 2000
This one is tidy. France is ISO 3166-1 numeric 250, and its code is 2250. The United States is 840, and its code is 2840.
region_code = 2000 + iso_3166_1_numeric
So you can accept ordinary country codes from users and convert.
The free-text field is the useful one
Field 3.12.1 is the one worth knowing about. It accepts a brand name or a bare domain:
{"2": 10, "3": {"12": {"1": "nike.com", "2": true}}, "7": {"1": 1, "2": 0, "3": 2250}}
That returns ads for Nike, Inc. without ever touching an advertiser ID. Most tooling in this space makes you go hunt the AR... ID by hand first.
Advertiser lookup, no auth required
POST /anji/_/rpc/SearchService/SearchSuggestions?authuser=
f.req={"1": "decathlon", "2": 10, "3": 10, "5": {"1": 1}}
Each result is either an advertiser (field 1) or a domain (field 2):
{"1": {"1": "DECATHLON FRANCE", "2": "AR12956355609636110337", "3": "FR",
"4": {"2": {"1": "6", "2": "6"}}, "5": true}}
Name, advertiser ID, country, an approximate ad-count range, and a verified flag. No token needed.
The endpoint you cannot use
LookupService/GetCreativeById returns per-ad country data and creative variations. It also requires an x-framework-xsrf-token header.
That token is embedded in the page HTML as xsrfToken: '...' — but only when you are signed in to Google. Fetch the same page anonymously and the value is simply absent. An anonymous scraper cannot get one.
Workaround: if you want to know which countries served an ad, search each country separately and record which ones returned it. Same answer, public endpoint only.
Ad copy is hidden in plain sight
This was the surprise. Creatives point at a content.js bundle on displayads-formats.googleusercontent.com. It looks like a 150 KB renderer blob, and my first assumption was that extracting ad text would need a headless browser.
It does not. The ad document is embedded inside that bundle as a JavaScript string full of escaped hex. Un-escape it and you get real HTML back:
import re
HEX = re.compile(r"\\x([0-9a-fA-F]{2})")
UNI = re.compile(r"\\u([0-9a-fA-F]{4})")
def unescape(js: str) -> str:
out = HEX.sub(lambda m: chr(int(m.group(1), 16)), js)
out = UNI.sub(lambda m: chr(int(m.group(1), 16)), out)
return out.replace("\\/", "/")
From the result you can pull the landing page, the display domain, the creative dimensions and the on-creative text.
Two traps I hit:
- Shopping ads are assembled by feed vendors (Criteo, Yteo and friends). The vendor host appears in the bundle before the advertiser link, so a naive "first non-Google URL" grabs the wrong company. Prefer a URL whose host matches the creative display domain.
-
Feed templates leave placeholders behind like
[Frais de livraison]and bare price fragments. They are not ad copy.
The bot wall
Google blocks a single IP after roughly 60 to 80 requests and redirects to /sorry/.
The important part: backing off does not clear it. I retried with exponential backoff for 21 seconds and stayed blocked. Only a different exit IP clears it. If you are paginating a large advertiser, rotate proxies mid-run and retry the same cursor, otherwise you lose everything collected so far.
Fill rates, honestly
Enrichment depends entirely on what kind of ads the brand runs. Two real measurements, 120 ads each:
| Brand | Creative size | Display domain | Creative text |
|---|---|---|---|
| decathlon.fr (feed/shopping ads) | 83% | 81% | 37% |
| nike.com (mostly plain images) | 20% | 12% | 20% |
Plain image ads ship no data bundle at all. Nothing to extract, for anyone.
Disclosure
I packaged this into an Apify actor: Google Ads Transparency Scraper. Search by brand or domain, filter by country, format and date.
But the field map above is the useful part and it is yours whether you use the actor or not. Happy to answer questions about the format in the comments.
Top comments (0)