Google Ads Advertiser Check & Transparency Scraper avatar

Google Ads Advertiser Check & Transparency Scraper

Pricing

from $0.70 / 1,000 ad creatives

Go to Apify Store
Google Ads Advertiser Check & Transparency Scraper

Google Ads Advertiser Check & Transparency Scraper

Check if a domain runs Google Ads, identify advertiser accounts, or export current creatives from Google's Ads Transparency Center. Search by domain, brand, advertiser ID, or public URL; filter by country, format, surface, and date; track when domains start or stop advertising. JSON/CSV/API/MCP.

Pricing

from $0.70 / 1,000 ad creatives

Rating

0.0

(0)

Developer

Brenton Keller

Brenton Keller

Maintained by Community

Actor stats

0

Bookmarked

17

Total users

12

Monthly active users

6 days ago

Last modified

Share

Google Ads Transparency Center Scraper

Pull every ad an advertiser is running on Google (Search, YouTube, Shopping, Maps, Play) from the Ads Transparency Center, as clean JSON/CSV. Search by domain, brand name, advertiser ID or a pasted Transparency Center URL. Filter by country, format, platform and date. Run it on a schedule with "only newly observed ads" to track changes in Google's result window. Use advertiser-check mode to answer "is this company running Google Ads, and how many?" for thousands of domains at a fraction of a cent each.

No browser, no login. Talks to the Transparency Center's own data API directly with a browser-grade TLS fingerprint, paces itself under Google's rate limits and rotates proxies automatically, so runs finish fast and don't fail halfway.

Try it free: every Apify account includes free monthly usage, and 1,000 ads cost $1, so checking a competitor is free.

TL;DR

You want to know…InputYou getCost
Is this company running Google Ads? (for a list of domains){"mode": "advertisers", "domains": [...]}One row per advertiser: is_actively_advertising (ad in the last 30 days), is_likely_owner, ad counts, billing country~$0.0033 per domain
Tell me when they start or stopadd "trackChanges": true, schedule weeklyactivity_change: started / stopped / still_active… ("stopped" lags ~30 days)same, no extra charge
What ads is a competitor running?{"domains": ["competitor.com"]} or {"queries": ["Brand"]}One row per ad: format, first/last shown, countries, preview URL$1 per 1,000 ads
Only the new ones, each weekadd "onlyNewAds": true, scheduleAds your previous runs had not seen in Google's result window$1 per 1,000 new ads

Filter on is_likely_owner to drop affiliates and resellers. No start fee, no subscription; failed domains are free. A run cut off by its timeout keeps every domain it finished. Everything below is detail — jump to Lead qualification at scale for bulk domain checks or Output for every field.

Start here: a bounded, useful first run

The Store form is prefilled with an advertiser check on three domains, the same input as Lead qualification Step 1. Click Start and you get about five rows for roughly $0.01, covering the three answers this mode gives: a domain whose owner advertises (figma.com → Figma INC, is_likely_owner: true), one that advertises at lower volume (posthog.com), and a clean no (sqlite.org, is_advertising: false). Replace the three domains with your own list; each costs ~$0.0033 on average, and that is the whole bill.

{
"domains": [
"figma.com",
"posthog.com",
"sqlite.org"
],
"mode": "advertisers",
"region": "ANYWHERE"
}

Second step: the ads themselves

To see what a competitor is running, use the input below (or in the form: mode Ads, Max ads per query 20, details on). 20 enriched rows show creative previews and every rendering variation, dates, and the countries each ad ran in, for at most $0.08; fewer rows cost less.

A note on what a US-only brand will and will not show: impression ranges and surfaces are published by Google only for EU-served and political ads, so a purely domestic advertiser returns null for both. Point the run at an EU-serving brand if those are what you came for.

{
"domains": [
"allbirds.com"
],
"mode": "ads",
"region": "US",
"includeDetails": true,
"maxAdsPerQuery": 20
}

Replace allbirds.com with one competitor domain and click Start. The cap is per query. Domain search means “ads link to this domain,” not “the domain owner bought every ad”; inspect advertiser_name and use its advertiser_id for a precise recurring watchlist.

Competitor ads monitor: schedule it, track Google's result window

Use a daily run to return only creative IDs the watchlist has not observed before, delivered to Slack or email. Prefer advertiser IDs over domains once you identify the right accounts: they avoid reseller/affiliate ads that happen to link to the same domain. Three steps, no code:

  1. Create a task from this actor with the input below. Replace the advertiser IDs and give the watchlist a unique monitorStoreName. Run it once; that first run seeds the list, so do not treat its returned rows as changes.

  2. Schedule it: in the task, open Schedule (or Console → Schedules) and pick daily. Each run charges only creative IDs not previously observed in the capped result window; a quiet day costs a few requests and no result-event charges.

  3. Alerts: in the task's Integrations tab add Slack or Gmail on "run succeeded". Use the run's status message as the notification text; in monitor mode it reports newly observed result-window ads or that none were observed. Slack message template (Handlebars):

    *Google Ads monitor:* {{resource.statusMessage}}
    <https://console.apify.com/storage/datasets/{{resource.defaultDatasetId}}|Open the newly observed ads>

    For "only notify when there is something new", route the run through n8n / Make / Zapier (Apify triggers exist for all three) and filter on the dataset item count.

{
"advertiserIds": [
"AR18378488041124659201",
"AR16735076323512287233"
],
"mode": "ads",
"region": "US",
"onlyNewAds": true,
"monitorStoreName": "footwear-competitors-daily",
"includeDetails": false,
"maxAdsPerQuery": 20
}

Each newly observed row carries is_new: true, first_seen_by_monitor, the creative preview URL, format, first/last shown and (with includeDetails) served countries and surfaces. This means newly observed, not necessarily newly launched. Google's capped top-N result window can reorder between runs, so a creative first shown months or years ago can enter the window and appear for the first time. Use first_shown to identify likely launches; narrow by advertiser ID, region, format, surface or date to make the window more stable. Use one monitorStoreName per watchlist.

An ad is remembered as seen only once it has reached your dataset. If a run stops at its charge limit, or Google blocks a later page of results, the ads it did not deliver come back as new on the next run instead of being lost.

What it costs at scale: 50 newly observed ads/day with details = 50 × $0.004 = $0.20/day, ~$6/month; without details, $1.50/month. Result-window reordering can increase this count on broad or heavily capped queries.

Weekly advertiser check: a low-cost retention workflow

Use advertisers mode when the recurring question is simpler: which watched domains have Google ads, under which observed advertiser accounts, and roughly how many creatives? Create a Task with this input, run it once, then schedule it weekly. Keep each week's dataset or export it as CSV to compare snapshots.

{
"domains": [
"allbirds.com",
"warbyparker.com",
"bombas.com"
],
"mode": "advertisers",
"region": "US",
"maxAdvertisersPerQuery": 3,
"trackChanges": true
}

From the second run on, every row says whether the domain started or stopped advertising since the last one, and the run's status message leads with the count. "Stopped" lags ~30 days behind the last ad; see Step 4 for why and how it handles failures.

Budget about 30 minutes per 300 domains and split lists above ~500 across runs — see "How long a run takes". Every domain gets an answer: one or more advertiser rows, or one is_advertising: false row. Treat it as observed Transparency Center activity, not ownership or spend. A domain can include affiliates/resellers, creative counts are Google ranges, and absence in one region does not prove global inactivity.

Lead qualification at scale

The question: "Here are 500 prospect domains. Which ones actually run Google Ads?"

If a company buys Google Ads, it has a paid-acquisition budget, someone who owns it, and an agency or tool stack. That makes "is this domain advertising?" a useful qualifier for ad-tech, agency, martech and competitive-intelligence outbound — and it is the one question this Actor answers cheaply in bulk, because advertisers mode charges per advertiser row, not per ad.

Step 1 — the input

Replace domains with your prospect list. Up to ~500 per run; see batching below.

{
"domains": [
"figma.com",
"posthog.com",
"sqlite.org"
],
"mode": "advertisers",
"region": "ANYWHERE",
"maxAdvertisersPerQuery": 3,
"requestDelaySeconds": 1.5
}

Those three domains are a deliberate smoke test — one advertiser whose name matches its domain (figma.com → Figma INC), one that advertises under a matching name at lower volume (posthog.com → PostHog Inc.), and one that does not advertise at all (sqlite.org). Run it once and confirm you get all three shapes before you paste in 500.

Keep region: "ANYWHERE" for qualification. A country filter answers "advertising in the US", which will mark a live advertiser as false if they only buy elsewhere. maxAdvertisersPerQuery: 3 is the right cap here: you want the owner plus a little context, not the full affiliate tail.

Step 2 — read the output

Every input domain gets at least one row, so the output joins back to your list 1:1 with no gaps. The columns that matter for qualification:

ColumnUse it for
is_advertisingDid anyone's ads point at this domain? Literally true, includes affiliates — and includes ads that stopped long ago.
is_actively_advertisingDid the domain show an ad in the last 30 days? The column for "actively running Google ads". days_since_last_ad has the number if you want a different cut-off.
is_likely_ownerIs this advertiser plausibly the company itself? The column to filter on.
has_significant_advertiserSame value on every row for a domain: did any returned advertiser look like the owner? Filter a bulk export on this.
name_matches_domainRaw signal: the advertiser's registered name contains the domain's brand word. Use when false positives cost you more than false negatives.

Three outcomes you will see, using real measured results:

figma.com is_advertising=true is_likely_owner=true name_matches_domain=true 'Figma INC' 1000-2000 ads
is_advertising=true is_likely_owner=false name_matches_domain=false 'Obra Studio' 6 ads
-> QUALIFIED. Figma itself advertises. The second row is unrelated noise; is_likely_owner removed it.
Expect that noise row to change between runs — on 2026-10-03 it was a 1-ad Australian company,
on 2026-10-07 a 6-ad studio. The owner row is the stable one.
airtable.com is_advertising=true is_likely_owner=true name_matches_domain=false 'FORMAGRID INC' 1000-2000 ads
-> QUALIFIED. Airtable's legal name is Formagrid, so the name signal misses it and the volume
fallback catches it. This is why you filter on is_likely_owner, not name_matches_domain.
sqlite.org is_advertising=false is_likely_owner=false has_significant_advertiser=false
-> NOT QUALIFIED. Nobody's ads point here.
venice.ai is_advertising=true is_actively_advertising=false days_since_last_ad=330 'Venice.ai'
-> STOPPED. It ran ads, but the newest one Google shows was last seen 2025-11-11. Measured
2026-10-07; 5 of the 80 advertising domains in our 100-domain run looked like this.
datadog.com is_advertising=false + a `hint` field
-> RECHECK. No ad exposed this landing domain, but Google's index knows 'DATADOG, INC.'.
Display/image-only ads carry no landing domain. The hint tells you how to re-query.

Step 3 — filter

Export the dataset as CSV and keep the qualified rows:

# one row per domain that has a plausible owner advertising
curl -sL "https://api.apify.com/v2/datasets/<DATASET_ID>/items?format=csv&clean=true" \
| python3 -c "
import csv,sys
r=csv.DictReader(sys.stdin); w=None
for row in r:
if row['is_likely_owner'] == 'true':
w = w or csv.DictWriter(sys.stdout, fieldnames=['query','advertiser_name','country','creatives_count_low','creatives_count_high'], extrasaction='ignore')
if r.line_num == 2: w.writeheader()
w.writerow(row)
"

Or ask the API for it directly — has_significant_advertiser is the one to use when you want a domain-level yes/no rather than per-advertiser rows:

$curl -sL "https://api.apify.com/v2/datasets/<DATASET_ID>/items?clean=true&fields=query,is_likely_owner,has_significant_advertiser,advertiser_name,creatives_count_high"

Tiering by spend proxy: creatives_count_high on the is_likely_owner row is a rough budget signal. Measured examples: figma.com 1,000–2,000 creatives, posthog.com 500–600, cal.com 36. A domain with thousands of live creatives is running a different operation from one with 36. Treat it as an order of magnitude, not a number — these are Google's own buckets.

Cost

advertisers mode charges $0.002 per advertiser row at the Free tier (less on paid plans — see Pricing). Cost scales with rows, and most prospect domains return one row.

Measured on a 22-domain audit corpus: 1.58 rows/domain across small/niche and non-advertising domains, 2.41 including mega-brands, and a hard ceiling of 3.00 at maxAdvertisersPerQuery: 3.

DomainsTypical rowsTypical costWorst case (cap of 3)
100~158~$0.32$0.60
500~790~$1.58$3.00
2,000~3,160~$6.32$12.00

Budget ~$0.33 per 100 domains. Domains with no ads are the cheapest outcome: one row each.

Runtime and batching

Google's rate limits set the pace — about 4 requests per domain. At the default requestDelaySeconds: 1.5:

DomainsApprox. runtime
100~10 min
300~30 min
500~50 min

A healthy bulk run looks idle for a long time. The status message carries a measured ETA after the first domain, and warns you in the first few seconds if the job cannot finish inside the run's timeout. If you see that warning, split the list rather than raising the delay.

Keep batches at 300–400 domains. That finishes inside a default timeout with headroom and is the batch size this workflow is actually proven at. Lowering requestDelaySeconds below 1.0 trades reliability for speed and raises the chance Google bot-blocks the run — the Actor retries, but a blocked target returns an error row and is not charged.

Step 4 — schedule it weekly, and let it tell you what changed

Prospect lists go stale: companies start and stop advertising. Re-running weekly turns a one-off list into a change feed, and "started advertising since last week" is a far stronger outbound trigger than "advertises". Add "trackChanges": true and the Actor keeps the diff for you. (If you would rather diff weekly exports yourself, every row carries a checked_at timestamp.)

"Stopped" is reported about 30 days late — by design. A domain counts as advertising while an ad has been shown in the last 30 days (is_actively_advertising). So "started" appears on the first run after a new ad goes live, but "stopped" appears only once 30 days have passed since the domain's last ad, not the day the campaign is paused. Google does not publish "campaign paused"; an ad that has not been seen for a month is the earliest reliable sign. If you need the raw number, filter on days_since_last_ad yourself, e.g. > 7, and accept more false "stops" from ads that run in bursts.

With trackChanges on, every advertiser-check row gains three columns. With it off they are absent, not null, so existing exports, CSV column layouts and Clay mappings see exactly what they saw before. If you append tracked and untracked runs into one sheet, read a missing activity_change as "not tracked":

FieldMeaning
activity_changestarted, stopped, still_active, still_inactive, first_check (no history yet), or null
previous_checkdate this domain was last checked with this store and these filters
state_sincedate this store first saw the current state — "inactive since 2026-10-07"

The run's status message leads with the count — "3 domain(s) started and 1 stopped advertising since the last check" — which is the line Apify's email and Slack run notifications quote, so a scheduled Task becomes an alert with no code. The dataset's Changes view shows the columns side by side; filter activity_change to started or stopped for the week's triggers.

Measured on a real two-run check (2026-10-07, state hand-edited between runs to force both flips): allbirds.com → started, venice.ai → stopped, sqlite.org → still_inactive, with the same 8 Google requests as an untracked run.

How it behaves when things go wrong — it errs toward reporting a change twice, never toward losing one:

  • A failed lookup is never a "stopped". A blocked or errored domain gets activity_change: null and its stored state is left alone; the next successful run compares against the last good one.
  • Only delivered rows record anything. State is saved per domain as its rows reach the dataset, so a timed-out or aborted run keeps the changes it delivered and records nothing for the rest, and a run that hits your Maximum total charge cannot silently consume a change you never saw.
  • Filters are separate histories. Changing region, surface, format or the date range starts a new history (first_check) rather than comparing two different questions.
  • The first tracked run reports every domain as first_check; changes start from the second.

It is free: no extra charge event, no extra Google requests. State lives in a named key-value store in your own account (changeStoreName, default ads-advertiser-check-state), one small record per domain. Use one store name per list you track.

Setting it up:

  1. In Apify Console, open the Actor → Create Task, paste the input with "trackChanges": true, and save.
  2. Run the Task once and confirm the row count matches your domain count (every row reads first_check).
  3. Attach a Schedule (weekly). For several batches, create one Task per batch and stagger them by an hour so they do not compete for the same IPs.
  4. Each week, filter activity_change to started / stopped; has_significant_advertiser tells you whether the advertiser is the company itself. Without trackChanges, diff is_actively_advertising per query between weeks' datasets yourself.

Parallel batches can share one changeStoreName. State is stored per domain, each record remembers when the domain was observed, and a run compares against the store as it is just before it delivers rows and never overwrites a newer observation, so a slow batch cannot roll back what a faster one recorded. A row whose observation was overtaken by a newer one from another run reads activity_change: null. One residual race remains, because Apify's key-value store has no compare-and-swap: two runs that finish within the same few seconds on the same domain can both report against the same prior state. If your lists overlap and that matters, give each list its own changeStoreName.

changeStoreName must be letters, digits and hyphens (not at the start or end), at most 63 characters — Apify's own rule. An invalid name fails the run immediately with a message saying so, before anything is scraped or charged.

Known limits, measured

  • A domain whose owner buys no ads can still get an owner. When no advertiser's name matches, the largest advertiser is flagged. weather.gov returns TPT NOW and irs.gov returns an individual, both is_likely_owner: true. Both carry name_matches_domain: false — if false positives are expensive for you, filter on name_matches_domain instead, accepting that it also rejects legitimate cases like FORMAGRID INC on airtable.com.
  • Very short brand words collide. cal.com resolves Cal.com, Inc. correctly, but any advertiser with "Cal" as a whole word in its name would also match.
  • is_advertising: true is not "the company advertises." It means someone's ads pointed at the domain. That is the whole reason is_likely_owner exists.
  • An empty result is not always a no. Google's domain filter only matches creatives that expose a landing domain, so display/image-only advertisers are invisible to it. Rows carry a hint when Google's own index contradicts the empty result.

Google Shopping Ads Check

Same mode, one extra field: surface: "SHOPPING" narrows every count and every yes/no to Google's Shopping surface, so you can ask "does this brand run Google Shopping ads?" across a list of e-commerce domains.

{
"domains": [
"allbirds.com",
"warbyparker.com",
"figma.com",
"sqlite.org"
],
"mode": "advertisers",
"surface": "SHOPPING",
"region": "US",
"maxAdvertisersPerQuery": 1
}

Real output, 2026-10-07, region: US:

domainis_advertisingadvertiserShopping adsread it as
allbirds.comtrueAllbirds Inc86runs Shopping campaigns
warbyparker.comtrueWarby Parker Inc1,000–2,000runs Shopping campaigns at scale
figma.comtrueFigma INC1technically present, not a Shopping seller
sqlite.orgfalse—0no Google ads at all

The filter is real, not cosmetic: allbirds.com returns 800–900 ads on surface: "ALL" and 86 on SHOPPING. And figma.com is why you should threshold on the count, not the boolean — a single Shopping ad from a SaaS company is noise. creatives_count_low >= 5 is a reasonable cut; pick your own.

What this measures, and what it does not

It measures paid Google Shopping campaigns (Product Listing Ads). It does not measure everything a brand sells on Google Shopping. Google Shopping also carries free Merchant Center listings, and those are not ads, so they appear nowhere in the Ads Transparency Center.

So the honest reading of a true is "this brand advertises on Google Shopping" — which is the stronger buying signal anyway, because it means a live paid-acquisition budget. A false means "no Shopping ads found", not "does not sell on Google Shopping": a brand with a Merchant Center feed and no paid campaigns reads as false here. If your use case needs free listings too, this is the wrong tool and you want a Shopping-page scraper instead.

A filtered empty result says so explicitly. notion.so with surface: "SHOPPING" returns:

No ads matched domain notion.so on SHOPPING, but Google's index knows this domain and the advertiser 'Notion Labs, Inc' (AR13309761427309854721). That advertiser runs ads somewhere, just not on SHOPPING for this domain — so read this as a "no" for SHOPPING.

That is a true negative for Shopping, and the hint tells you Notion advertises elsewhere rather than leaving you to guess.

Cost and runtime

Identical to any other advertiser check — the surface filter is free, and it does not change the row count:

  • $0.002 per domain (Free tier) with maxAdvertisersPerQuery: 1, flat, whether the brand advertises or not. 1,000 brands = $2.00. Bronze $0.0018, Silver $0.0016.
  • Measured 3.5 s per domain, ~3 requests. 1,000 brands ≈ 60 minutes, so split into 2–4 runs (see "How long a run takes").
  • Works in ads mode too, if you want the Shopping creatives themselves rather than a yes/no — same filter, priced per creative instead of per advertiser.

surface also accepts SEARCH, YOUTUBE, MAPS and PLAY, so the same pattern answers "does this brand run YouTube ads?" without changing anything else.

What it does

ModeOne row perUse it for
ads (default)ad creativecompetitor creative research, ad libraries, longevity analysis (which ads run for months), creative monitoring
advertisersmatched advertiserlead enrichment / qualification: is this domain advertising on Google right now, under which account(s), roughly how many ads

Who it's for

  • Performance marketers and agencies tracking competitors' Google ads: result-window change alerts, formats, and how long each creative survives (an ad-spy tool for Google, YouTube and Shopping ads at pay-per-result prices).
  • Sales and lead-gen teams enriching prospect lists (Clay, n8n, Make, Zapier, Google Sheets) with "runs Google Ads: yes/no + volume" as a buying signal.
  • Brand and compliance teams watching for affiliates, resellers or impersonators advertising on their domain.
  • Researchers and journalists studying advertising activity by region and platform.
  • AI agents: every field is stable and typed, and the actor is exposed as an MCP tool via Apify.

Input examples

All ads for a domain, US, last 90 days, YouTube only

{
"domains": ["allbirds.com"],
"region": "US",
"surface": "YOUTUBE",
"dateFrom": "2026-06-01",
"maxAdsPerQuery": 500
}

A brand by name, with full per-country details

{
"queries": ["Nike"],
"maxAdvertisersPerQuery": 1,
"includeDetails": true
}

Paste Transparency Center URLs

{
"startUrls": [{ "url": "https://adstransparency.google.com/advertiser/AR18378488041124659201?region=GB" }]
}

Monitor by advertiser ID instead of domain (see the monitor section above for scheduling and alerts)

{
"advertiserIds": ["AR18378488041124659201", "AR16735076323512287233"],
"onlyNewAds": true,
"monitorStoreName": "footwear-competitors"
}

Lead enrichment: is each domain advertising?

{
"domains": ["allbirds.com", "warbyparker.com", "some-local-plumber.com"],
"mode": "advertisers",
"region": "US"
}

Output

ads mode

{
"creative_id": "CR04143963882324492289",
"advertiser_id": "AR18378488041124659201",
"advertiser_name": "Nike Retail BV",
"format": "TEXT",
"matched_domain": "nike.com",
"first_shown": "2026-07-22T04:31:55+00:00",
"last_shown": "2026-09-01T21:04:44+00:00",
"days_shown": 43,
"impressions": null,
"impressions_low": null,
"impressions_high": null,
"preview_url": "https://tpc.googlesyndication.com/archive/simgad/12774179880874022668",
"preview": { "script_url": "…", "iframe_url": null, "html_snippet": null, "image_url": "…" },
"ad_url": "https://adstransparency.google.com/advertiser/AR18378488041124659201/creative/CR04143963882324492289?region=anywhere",
"advertiser_url": "https://adstransparency.google.com/advertiser/AR18378488041124659201?region=anywhere",
"query": "nike.com",
"query_kind": "domain",
"region": "US",
"matched_total": { "low": 100000, "high": 200000 },
"advertiser_ads_on_domain": { "low": 80000, "high": 90000 },
"advertiser_share": null,
"share_of_top_advertiser": null,
"name_matches_domain": true,
"is_likely_owner": true,
"is_new": true,
"checked_at": "2026-10-12T19:40:07Z"
}

Every row in both modes, error rows included, carries checked_at: when that target was checked, in UTC to the second. It is stamped per target, so rows from a 40-minute batch show when each domain was actually looked at. Use it to diff two exports yourself without trackChanges.

Separating the brand from the affiliates

A domain search returns every advertiser whose ads link to that domain. nike.com returns Nike's own entities and a media agency, three regional resellers and a private individual running one ad. microsoft.com mixes Microsoft's corporate entities with Veeam and two unrelated startups. Nothing is filtered out — brand-protection users want exactly that reseller data — so every row carries the fields to filter it yourself:

fieldmeaningpopulatedstable across run sizes?
is_likely_ownerthis advertiser plausibly owns the domain — the field to filter onalwaysyes
name_matches_domainthe advertiser's registered name contains the domain's brand wordalwaysyes
advertiser_ads_on_domainhow many ads this advertiser runs on this domain (Google's bucketed range)for the advertisers the run priced: the name matchers, or all of them when nothing matchedyes
share_of_top_advertiserthis advertiser's ads as a fraction of the domain's largest advertiser, 0–1only when no advertiser's name matched the domainyes
advertiser_sharethis advertiser's slice of the advertisers priced in your result windowonly when no advertiser's name matched the domainno — see below
count_lookup_failedtrue when this advertiser's count request failed (for example, rate limited)always—

The first two are read off the advertiser's name and cost nothing, so they are on every row. The count comes from one request per advertiser, and the two ratios need that count for every advertiser on the domain, because that is their denominator. When a name match has already settled ownership the actor prices the matchers and stops, so the owner keeps a real advertiser_ads_on_domain and both ratios are null rather than a ratio over the one or two advertisers that were priced — which would read 1.0 on every owner row. See "What these fields cost you" below.

is_likely_owner means the same thing in ads and advertisers mode, so you can join the two datasets:

  1. If any advertiser on the domain has a name matching the domain's brand, the name matchers are the owners and everything else is noise.
  2. If none does, fall back to the single largest advertiser, provided it runs at least 3 ads and is not tied with another. This catches owners whose legal name differs from their brand (awaytravel.com → JRSK, Inc., basecamp.com → 37signals LLC).

Volume alone cannot do this job: on notion.so the real Notion entity and a Brazilian motorcycle dealer both run exactly 5 ads, and hims.com is owned by an advertiser with 2.

Real output for microsoft.com:

advertiserads on domainname_matches_domainis_likely_owner
Microsoft Corporation90,000–100,000truetrue
Microsoft Limited900–1,000truetrue
Microsoft40–50truetrue
Veeam Software Group GmbHnullfalsefalse
Amplify Ventures Limitednullfalsefalse
Fathom Data Labs Pte Ltdnullfalsefalse

(The three Microsoft entities matched the domain name, which settles ownership, so the actor did not spend a request counting the other three advertisers' ads — hence null. They are still returned, with all their ads.)

So the competitive-research filter is just is_likely_owner == true, and inverting it gives you the resellers, affiliates and impersonators on your own domain. For a stricter cut, require name_matches_domain == true as well — that drops the volume fallback and with it the one failure mode below.

What these fields cost you: no extra rows, so no extra charge, and as little run time as the verdict allows. Name matching is free, so when any advertiser on the domain matches the domain's brand, the owner question is already answered and the actor prices only the matching advertisers. On apple.com that is 2 requests, not the 20 the cap allows, with the other 29 advertisers in the window left unpriced. Only when nothing matches does it price the whole list — one request each, capped at 20 — because then the counts are the verdict. Those unpriced rows carry advertiser_ads_on_domain: {low: null, high: null}, and both share fields are null on every row of that domain (a share of the 2 advertisers we priced would read as a share of the domain, which it is not). is_likely_owner and name_matches_domain are identical either way. If you need a count for every advertiser on a domain rather than for its owner, run advertisers mode, which prices each advertiser it returns.

Where this is weak

  • A domain whose owner buys no ads. weather.gov has no Google advertiser of its own, so the volume fallback promotes TPT NOW — a third party whose ads happen to link there. Same on irs.gov. Requiring name_matches_domain == true avoids this entirely.
  • Whole-word brand collisions. Matching is on word boundaries, so Caltex does not match cal.com and Metamucil does not match meta.com. But a real unrelated company that is the brand word still matches: Cal Poly Pomona on cal.com, Meta Financial Group on meta.com.
  • Wholly non-Latin names cannot match. A registered name written entirely in CJK characters tokenizes to nothing, so it can never match a Latin brand word and depends on the volume fallback.
  • Counts are buckets. Google reports 900–1,000, not 947, so advertiser_share and share_of_top_advertiser are approximate.
  • The share fields are null whenever a name match decided ownership, which is the common case on brand domains. Filter on is_likely_owner / name_matches_domain; those are always populated. The counts are there for the case where they matter, which is the domain with no name match at all.
  • advertiser_share moves with your window size when it is populated. It is the advertiser's slice of the advertisers priced in your run, and a larger maxAdsPerQuery discovers more of them — on cal.com, Cal.com, Inc. read 0.63 at 10 ads and 0.42 at 40. The owner verdict does not move; if you need a stable number, use the raw advertiser_ads_on_domain.
  • A failed count lookup means no volume verdict. If any advertiser's count request fails on a domain with no name match, is_likely_owner is null on that domain's rows instead of false, the ratios are null, and the failed advertiser's rows carry count_lookup_failed: true. A missing count is not a zero, so the actor will not name an owner from the advertisers it happened to price. Re-run the domain for a verdict.
  • Domains with no ads produce no rows at all in ads mode, so there is nothing to carry is_likely_owner. Use advertisers mode for the "does this company advertise at all?" question — it returns an explicit is_advertising: false row for every domain.

On non-domain queries (advertiserIds, queries) there is no domain to take a share of: the share fields are null and is_likely_owner is true, since you named the advertiser yourself.

With includeDetails: true each row also gets:

"details": {
"country_reach": [{ "country": "US", "first_served": "2026-07-22", "last_served": "2026-09-01", "impressions": null, "surfaces": [] }],
"eu_reach": { "impressions": { "low": 10000, "high": 50000 }, "first_served": "2026-07-21", "last_served": "2026-09-01" },
"variations": [ { "script_url": "…", "image_url": "…" } ],
"targeting": { "categories": { "1": {"1": true, "2": false} } },
"category": { "gcb_category_id": 10021 },
"contains_synthetic_media": false
},
"countries": ["US", "GB", "DE", "FR", "CA"],
"surfaces": ["SEARCH", "YOUTUBE"]

How often each detail field is actually populated. Measured over 225 enriched rows across 9 US/EU retail domains, so treat the thin numbers as indicative rather than exact:

fieldfillwhat you get
country_reach / countries100%The real reason to enable details — basic mode gives 0%. But 87% of ads list exactly one country.
variations (more than one rendering)84%Every rendering of the creative, where basic mode exposes a single preview_url.
customer_name72%Was byte-identical to advertiser_name in every populated sample, so it is now dropped when it duplicates that column and kept only when it genuinely differs.
targeting18%Not usable as shipped. age/gender/geo are always null and are no longer emitted; what remains is categories, which comes back as undecoded protobuf keys, as shown above.
category.gcb_category_id16%A bare numeric ID. The readable google_product_category fills on 1% of rows and vertical on none.
eu_reach14%EU-served ads only.
country_reach[].impressions, surfaces13%EU-served and political ads only — see below.
served_countries, us_reach, details.first_shown, details.days_shown0%Google does not return these; first_shown and days_shown are already at the top level at 100%.

Fields Google never populates are omitted from details rather than emitted as nulls, so a key's presence means it carries data. If Google starts returning any of them, they pass straight through again.

If you are researching a US-only brand, expect zero impression data and a one-element country list — allbirds.com, bombas.com and casper.com all returned 0% impressions. EU-serving advertisers do much better (56% of Nike's ads carried impressions). Enable details for country coverage and creative variations; do not buy it for targeting or category.

preview_url is not a stable key: Google randomises two query parameters on every response, so the same ad yields a different preview_url run over run (43% of rows in a back-to-back comparison). Use creative_id to dedupe or diff.

Impression and spend ranges are only published by Google for EU-served ads and political ads; elsewhere they are null. Where they exist they are lifted to the top level as impressions_low / impressions_high (global) and eu_impressions_low / eu_impressions_high (EU reach from the details block), so CSV exports and the Overview table show them without digging into details. matched_total is Google's estimate of how many ads match the whole query (useful even when you cap maxAdsPerQuery).

advertisers mode

{
"query": "allbirds.com",
"query_kind": "domain",
"region": "US",
"is_advertising": true,
"advertiser_id": "AR12345678901234567890",
"advertiser_name": "Allbirds Inc",
"creatives_count_low": 1000,
"creatives_count_high": 2000,
"advertisers_on_domain": 1,
"formats_seen": ["IMAGE", "TEXT", "VIDEO"],
"last_shown": "2026-09-02T01:46:39+00:00",
"advertiser_url": "https://adstransparency.google.com/advertiser/AR12345678901234567890?region=US",
"name_matches_domain": true,
"share_of_top_advertiser": 1.0,
"is_likely_owner": true,
"has_significant_advertiser": true,
"email_provider": "google_workspace",
"domain_last_shown": "2026-10-07",
"days_since_last_ad": 0,
"is_actively_advertising": true
}

Domains with no ads return one row with is_advertising: false, so every input gets exactly one answer.

Reading "is this company advertising?" without the noise

is_advertising: true does not mean the company advertises. Google's domain filter returns every advertiser whose ads link to a domain, so a check on notion.so legitimately returns

Nolan Properties PTY Limited
and NEPEAN REGIONAL SECURITY PTY LIMITED next to Notion itself. The data is correct; the question most people ask this mode is narrower. Four fields answer the narrower question, and is_advertising is left alone because it is literally true:

FieldMeaning
is_likely_ownerThis advertiser plausibly is the company behind the domain. The field to filter on.
has_significant_advertiserSame value on every row for the domain: did any returned advertiser look like the owner? Filter a bulk export on this to drop domains that returned only noise.
name_matches_domainRaw signal: the advertiser's registered name contains the domain's brand word. No judgement applied — use it when you want to be stricter than is_likely_owner.
share_of_top_advertiserThis advertiser's ad count as a fraction of the largest advertiser's on the same domain. 1.0 is the largest. Apply your own threshold.
count_lookup_failedtrue when the request for this advertiser's ad count failed (for example, Google rate-limited it). Its creatives_count_* are then null.
checked_atWhen this domain was checked, UTC ISO-8601 (2026-10-12T19:40:07Z). On every row, so two exports can be diffed without trackChanges.

How is_likely_owner is decided: if any advertiser's name matches the domain's brand word, only those are flagged. Volume alone cannot do this — measured on notion.so, the real Notion Labs Japan合同会社 and an unrelated Brazilian motorcycle dealer (DFS COMERCIO DE MOTOS LTDA) each had exactly 5 ads, so "most ads wins" is a coin flip. Nor can a minimum ad count: Hims, Inc. owns hims.com with 2 ads total, and any "fewer than 3 ads is noise" rule would discard the correct answer.

When no advertiser name matches the domain, we fall back to flagging the single largest advertiser, which is right for owners whose legal name differs from their brand — awaytravel.com is owned by JRSK, Inc. — and is why the field is named likely.

The fallback needs every advertiser's count. If any count request fails (count_lookup_failed: true), the largest advertiser is unknown, so is_likely_owner, has_significant_advertiser and share_of_top_advertiser are null for that domain rather than a guess from the counts that did arrive. A name match needs no count, so it still decides. The row is still delivered and charged: it carries the advertiser, its country and the recency columns. Re-run the domain if you need the verdict.

Where it is wrong, measured 2026-10-03. The volume fallback flags the largest advertiser even when the domain's real owner buys no ads at all. weather.gov returns TPT NOW (40 ads) and irs.gov returns Tenisha Austin (5 ads), both flagged is_likely_owner: true with has_significant_advertiser: true. Both are wrong in the sense a sales team cares about. Both also carry name_matches_domain: false, so if false positives cost you more than false negatives, filter on name_matches_domain instead — it would reject these two, at the price of also rejecting JRSK, Inc. on awaytravel.com. Very short brand words are the other weak spot: cal.com resolves Cal.com, Inc. correctly, but a hypothetical Cal Poly advertising on that domain would also match, because cal is a whole word in both.

Matching is on word boundaries, not substrings, so cal.com does not match Caltex and meta.com does not match Metamucil or Meta-Analysis. Brand words of five or more characters also match a longer word starting with them (ridge.com → Ridgewallet LLC); shorter ones do not.

Acronym brands match their spelled-out name. bbc.co.uk → BRITISH BROADCASTING CORPORATION, ibm.com → International Business Machines Corporation, ups.com → United Parcel Service, Inc.. Without this, bbc.co.uk flagged its media agency Havas Media Limited (200 ads) as the owner over both BBC entities. The rule is strict — 3 to 6 letters, exactly one word per letter — and on our 100-domain test list it fired on none of them, so it changes nothing for ordinary brand names.

For advertiserIds queries the domain fields are null: you named the advertiser exactly, so there is no owner question. Brand-name queries do get an answer, because a name is fuzzy — Google's suggestion index returns "Nike" → Nike (Kenya, 1 ad), Nikena, nikethan g… before Nike, Inc.. Candidates are matched on whole words (so Nikena is not "Nike"), ranked by size, and only the advertiser the name resolved to is is_likely_owner: true — Nike, Inc. (9,000+ ads) for "Nike", Notion Labs, Inc for "Notion". A name that matches nothing on whole words flags no owner at all. Use advertiserIds when you need one exact account.

is_likely_owner means the same thing in both modes: if any advertiser's name matches the domain, the matching advertisers are the owners; otherwise the single largest advertiser is, if it has at least 3 ads and no other advertiser ties it. (An earlier ads-mode version used a 5%-share volume test and flagged DFS COMERCIO DE MOTOS LTDA as a co-owner of notion.so; both modes now use the rule above.)

A zero-result row also tells you whether to believe it. When Google's index knows the domain or brand even though no creative matched, the row carries a hint naming what to retry with (see Limits and notes for why this happens and what it costs):

{
"query": "hims.com",
"is_advertising": false,
"creatives_count_low": 0,
"hint": "No ads matched domain hims.com, but Google's index knows this domain and the advertiser 'Hims, Inc.' (AR09131126584697159681). Display/image-only ads often expose no landing domain and are invisible to a domain search, so this may be a filter miss rather than a \"no\" -- to check, retry with queries: [\"hims\"] or advertiserIds: [\"AR09131126584697159681\"]."
}

The field is absent when the "no" is solid: sqlite.org returns no hint, hims.com returns the one above. Filter your export on the presence of hint to pull exactly the rows worth a second look.

Advertiser-check rows for a domain

Each advertiser row for a domain carries the advertiser's billing country from its Transparency Center profile — the single most useful field for telling the domain owner apart from affiliates and resellers — plus its own ad count for that domain (creatives_count_low / creatives_count_high), the unverified flag, and the domain-wide total in domain_creatives_count_low / domain_creatives_count_high. (legal_name is also returned, but in our samples it was always identical to advertiser_name.) This distinguishes the domain owner from resellers and affiliates that also link to the domain: a nike.com check returns Nike Retail BV at 90,000–100,000 creatives, Nike, Inc. at 8,000–9,000 and a Singapore branch at 1,000–2,000, against a domain-wide 100,000–200,000. Costs 2 extra requests per advertiser row; maxAdvertisersPerQuery caps it. Candidates are ranked by how many of the domain's sampled ads each advertiser owns before that cap is applied, so truncating drops the smallest affiliates rather than the owner, and the returned rows are ordered by each advertiser's true ad count for the domain. Google's own ordering is by most recent ad, which put a one-ad affiliate ahead of the owner on ~20% of domains in a narrow region (casper.com and awaytravel.com under region: DE both did); ranking by sample share fixes both of those cases. It is still a heuristic on a 40-ad sample — use country and the creative counts to confirm the owner, and keep maxAdvertisersPerQuery above 1 when the domain may have several legitimate advertisers.

email_provider — a free column on every advertiser-check row

Each row also carries email_provider, the domain's email host, from one MX lookup per domain. It is free: no charge event, and the lookup is not an Apify request, so a 300-domain check costs exactly what it did before.

ValueMeaning
google_workspace, microsoft_365the domain's mail is hosted there
proofpoint, mimecast, messagelabs, barracuda, cisco_ironport, sophos, trend_micro, fortineta security gateway fronts the mailbox. We report the gateway and do not guess what is behind it
zoho, fastmail, proton, amazon_ses_workmail, yandex, mailru, qq_tencent, alibaba, rackspace, godaddy, cloudflareother hosted providers
self_hostedevery MX is under the domain's own name
no_mxno MX record. Common and not an error: measured on 300 domains, 28% have none and two thirds of those still serve a website, so the company receives mail at a different domain
null_mxthe domain explicitly declares it accepts no mail (RFC 7505)
nxdomainthe domain does not resolve
otheran MX host we do not recognise
nullthe lookup failed, or the query was an advertiser ID or brand name rather than a domain

Useful as a company-maturity and tech-stack filter alongside "runs Google Ads": a domain on google_workspace or microsoft_365 is a going concern, and a gateway implies enterprise security spend. Two limits worth knowing: a gateway hides the provider underneath, and roughly a third of domains land on no_mx/null_mx/nxdomain, where there is no provider to report.

is_actively_advertising — is the domain advertising now? (free)

is_advertising answers "does the Transparency Center hold ads for this domain", and Google keeps ads there for years after they stop running. Three free columns, the same on every row for a domain, answer "now":

FieldMeaning
domain_last_shownDate the domain's most recent ad was last shown, by any advertiser
days_since_last_adDays from that date to the run
is_actively_advertisingtrue if that is 30 days or fewer. false for a domain with no ads. null for advertiser-ID and brand-name rows, which have no domain to date, and when some of the domain's ad dates could not be read and the readable ones are older than 30 days — the unreadable ad might be the recent one, so we do not answer false (and change tracking does not report stopped)

Measured 2026-10-07: allbirds.com and notion.so read 0 days; venice.ai 330, kentlive.news 237 and unicaen.fr 119 all still read is_advertising: true. The date costs nothing extra: Google returns a domain's ads newest first (checked on 10 domains, two pages each), so the first page we already fetch holds the newest one. It is the newest ad pointing at the domain, so an affiliate's recent ad keeps a domain active even when the owner has stopped — pair it with has_significant_advertiser for "the company itself is advertising".

How long a run takes

Google rate-limits by IP, so the actor paces itself at roughly one request per requestDelaySeconds (default 1.5 s). Runtime is therefore a function of requests, not rows, and a large batch is slow by design — a quiet run is working, not hung. The run's status message carries an estimate before the first request and then a measured "~N min left" from the first target onward, so you can size the next batch from the last one's real pace.

InputRequests per domainMeasured
advertisers mode, mixed domain list~4~30 min per 300 domains (our own batches: 1,735–3,000 s for 300–390 domains)
ads mode, maxAdsPerQuery: 40, no details2–4a few seconds per domain
ads mode with includeDetails: true1 per ad~40 s per 25 ads — this dominates everything else

Apify's default run timeout is 3,600 s. So:

  • Split domain lists above ~500 domains across separate runs or scheduled Tasks. We chunk our own 2,000-domain weekly pipeline four ways for exactly this reason.
  • Never combine a long domain list with includeDetails: true. 300 domains at 200 ads each is tens of thousands of requests and fits in no timeout; enrich one advertiser or a short list instead.
  • Raise Timeout in the run options if you would rather have one big run than several small ones.
  • If the estimate is longer than the time left on the run, the status message says so within the first few seconds and names a batch size that would fit — kill it then, not 50 minutes in.
  • A run that is cut off keeps everything it finished. Rows go into the dataset as each domain completes, so a run that hits its timeout at domain 340 of 390 delivers — and charges for — the first 340, and trackChanges state is saved for exactly those. Re-run the remainder.

Pricing

Pay per result, no subscription:

EventPrice
Ad creative (ads mode)$1.00 per 1,000
Ad creative with full details (includeDetails)$4.00 per 1,000
Advertiser check row (advertisers mode)$2.00 per 1,000

No per-run start fee. 10 of the 12 competing Ads Transparency actors we surveyed charge one, from $0.00005 to $0.20 per run. That matters most for the scheduled-monitor pattern, where you pay for a small number of rows many times over.

That is the whole bill. The actor is priced pay-per-event only: compute, proxy and storage are paid by us out of the event price, not added to yours. What you see in the table is what a run costs.

What counts as a billable row. Rows carrying data are charged, including rows with is_advertising: false — a verified "this domain has no ads in Google's index" is a result you asked for. Failed targets are not charged: a target that errored or was blocked mid-run still gets a row with an error field so the failure is auditable, but that row is written to the dataset with no charge event attached and costs you nothing. A run where every target is blocked fails outright and charges nothing at all. The run's status message reports how many error rows went out free.

email_provider and the recency columns are free too. Every advertiser-check row carries the domain's email host and when the domain last showed an ad at no extra cost: neither has a charge event, so a 300-domain check costs exactly what it did before the columns existed. See email_provider and recency.

In advertisers mode the unit you buy is a row, but the unit you think in is a domain — and they are not the same. A domain that advertises returns up to maxAdvertisersPerQuery rows; a domain with no ads returns exactly one. Measured over a 100-domain run: 1.63 rows per domain, so budget about $0.0033 per domain checked, not $0.002 (a 20-domain run with a higher share of non-advertisers came in at 1.35). Set maxAdvertisersPerQuery: 1 to pin it to exactly one row and $0.002 per domain, at the cost of missing co-advertisers — the single row you keep is now the advertiser with the largest share of the domain's sampled ads, which is the owner in every case we have measured, but it is a sample-based heuristic, not a guarantee.

Limits and notes

  • Google paginates to roughly 1,000 ads per query. Split large advertisers by format, surface or date range to get more.
  • Google keeps about 12 months of ad history.
  • A domain search returns every advertiser whose ads link to that domain, not only the domain owner. That can include resellers, affiliates and demo/test ads (example.com yields real ads from unrelated advertisers). Both modes mark which advertisers plausibly own the domain, so you can filter the brand’s own ads from the resellers without losing either (see “Separating the brand from the affiliates”). Use advertisers mode to see who they are, or advertiserIds to pin one account. The reverse also happens: a third party linking to a domain makes that domain look like an advertiser (irs.gov and weather.gov both return is_advertising: true), so read the result as "ads point at this domain", not "this company buys ads".
  • is_advertising: false means no creative in Google's index is associated with that domain, which is not the same as "this company runs no Google ads". Display/image-only advertisers frequently expose no landing domain, so they are invisible to a domain query: hims.com returns false while the advertiser "Hims, Inc." is live with current image creatives. The actor now checks this for you: when a domain matches nothing, it makes one extra suggestion lookup, and if Google's index knows the domain or a matching advertiser you get a hint field on the row and the same text in the run's status message, naming the advertiser ID to retry with. No hint means Google's index knows nothing either, so the empty result is a genuine "no". The lookup costs one request and only happens on the empty path; it is skipped in onlyNewAds monitor runs, where an empty result is the normal outcome.
  • Brand-name queries are resolved through Google's own suggestion index (top 50 candidates); names that contain the brand as whole words are ranked by size, and letters-only lookalikes (Nikena for "Nike") come last and are never flagged as owner. Use advertiserIds for precision.
  • Internationalized domains (bücher.de, müller.de) return no ads in either their Unicode or punycode form, because Google records the ASCII domain an ad links to. The row's hint names the spellings to retry with (mueller.de has 6,000–7,000 ads).
  • TEXT ads come as images, not words. Google serves 94% of TEXT creatives as a rendered image (measured over 902 creatives) and the rest as a script preview; the Transparency Center returns no headline or description text. You get the creative's image/preview URL and its dates, not the ad copy.
  • Ad-text (keyword) search is only available for political ads in Google's data and is not exposed here.
  • Runs are paced, not fast: budget ~30 min per 300 domains in advertisers mode and split lists above ~500 (see "How long a run takes"). A run that hits the timeout keeps the rows for every domain it finished.
  • Google rate-limits by IP. Keep the default Apify proxy on for anything beyond a few hundred ads; the actor rotates sessions on every rate-limit response and backs off, so runs complete instead of failing.
  • If Google blocks every request (bot-check page or HTTP 429 after retries), the run fails with a clear error and nothing is charged. Enable Apify Proxy (residential) or raise requestDelaySeconds. If only some targets are blocked, the run succeeds and those targets get a row with an error field — those rows are not charged.
  • Rows whose detail lookup failed (details_error) are charged at the base ad rate, not the enriched rate.
  • Monitor state is per monitorStoreName (a named key-value store in your account). Do not run two monitor runs against the same store at the same time; use one store per watchlist or schedule.

Use as an MCP tool / from code

MCP

This actor is a remote MCP server on Apify's hosted gateway — no install, no local process. Point any MCP client (Claude, ChatGPT, Cursor, or your own agent) at:

https://mcp.apify.com/?tools=brenton8907/google-ads-transparency-center

Authentication is Apify OAuth, or an Apify API token as Authorization: Bearer <token>. For clients that take a JSON config:

{
"mcpServers": {
"google-ads-transparency": {
"type": "http",
"url": "https://mcp.apify.com/?tools=brenton8907/google-ads-transparency-center",
"headers": { "Authorization": "Bearer ${APIFY_TOKEN}" }
}
}
}

The connection exposes the tool brenton8907--google-ads-transparency-center, which takes the same input documented above, alongside Apify's run/dataset helper tools (get-actor-run, get-dataset-items, get-key-value-store-record, abort-actor-run). It is listed in the official MCP Registry as io.github.brenton-keller/google-ads-transparency-center and as a Glama connector.

Agent pattern. Keep a watchlist of advertiser IDs, call with onlyNewAds: true on a schedule, and act only on the rows that come back. Charges are the actor's published pay-per-event prices — an agent calling this tool spends real money per row, so cap it with maxAdsPerQuery and prefer mode: "advertisers" for cheap yes/no qualification before pulling creatives.

From code

curl -X POST "https://api.apify.com/v2/acts/brenton8907~google-ads-transparency-center/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"domains": ["allbirds.com"], "mode": "advertisers", "region": "US"}'

Use with Clay, n8n, Make and Zapier

Clay: built-in Apify integration (included in all Clay plans):

  1. In your table, Add enrichment, search "Apify" and choose Run Apify Actor.
  2. Connect your Apify account (Clay runs the Actor on your Apify account, so you pay this Actor's per-row price and nothing else on the Apify side).
  3. Choose the Actor brenton8907/google-ads-transparency-center.
  4. In Input Data, enter {"mode": "advertisers", "domains": [<Domain column>], "maxAdvertisersPerQuery": 1}: insert your domain column as a token, with the keys in quotes and no quotes around the token. Spell out "mode": "advertisers"; without it the Actor runs in ads mode and returns individual ads instead of one verdict per domain.
  5. Run it on one row first, then map the fields you need into columns: is_advertising, is_actively_advertising, days_since_last_ad, advertiser_name, is_likely_owner, has_significant_advertiser, email_provider, hint.

With maxAdvertisersPerQuery: 1 each run returns one row per domain, so the enrichment stays 1:1 with your table. Filter on is_likely_owner (strict) or has_significant_advertiser (bulk), never on is_advertising alone, and add is_actively_advertising when the question is "running Google ads now". Add "region": "US" (or another country code) to the input to narrow the check; the default is ANYWHERE.

Alternative: Clay's HTTP API enrichment (Clay's Growth plan or higher), if you'd rather call the synchronous endpoint above directly. It returns the rows in the HTTP response, so there is nothing to poll. The same request works from any tool with a generic "call an HTTP API" step:

fieldvalue
MethodPOST
URLhttps://api.apify.com/v2/acts/brenton8907~google-ads-transparency-center/run-sync-get-dataset-items
Query paramstoken = your Apify API token · fields = query,is_advertising,is_actively_advertising,days_since_last_ad,advertiser_name,country,creatives_count_low,creatives_count_high,is_likely_owner,has_significant_advertiser,email_provider,hint · maxTotalChargeUsd = 0.01
HeaderContent-Type: application/json
Body{"domains": ["{{domain}}"], "mode": "advertisers", "region": "US", "maxAdvertisersPerQuery": 1}

In the HTTP column, map from the first array element — 0.is_advertising, 0.is_likely_owner, 0.advertiser_name, 0.creatives_count_low / _high — with the same filtering advice as above.

Either way it is one row per domain: $0.002 per domain, flat, whether the domain advertises or not. Watch for the hint field: a false with a hint means "display-only advertiser, invisible to a domain query", not "no".

Above ~500 domains it is cheaper and faster to run the Actor once in Console with the whole list, export the dataset as CSV and import that — same Apify cost, no per-row credits in your enrichment tool, one run instead of hundreds. See "How long a run takes" for batch sizing.

If a domain comes back false with a hint, route it to a second enrichment (Run Apify Actor or HTTP) whose input checks the advertiser ID the hint names: {"advertiserIds": ["AR..."], "mode": "advertisers"}.

Symptom in ClayCause and fix
401 / Authentication token is not validHTTP column: the token param is missing, truncated, or the Clay variable did not interpolate. Re-paste the token. Run Apify Actor: reconnect your Apify account in Clay.
408 Request TimeoutHTTP column: the call ran past 300 s. Send one domain per call, or run the whole list once in Console and import the CSV.
Column is blank, no errorHTTP column: you mapped is_advertising instead of 0.is_advertising. The response is an array.
One row per ad instead of one verdict per domain"mode": "advertisers" is missing from the input; the default mode is ads.
hint column always blankExpected: most rows have no hint (and the HTTP column's fields drops absent keys). Blank means there is no reason to doubt the no.
false for a brand you know advertisesregion is too narrow (use "ANYWHERE"), or the advertiser is display-only and invisible to a domain query; read the hint.
Calls queue or fail in bulkApify's concurrency ceiling. Add memory=256 to the query params, throttle the Clay column, or run in Console.
A row has an error fieldThat domain failed, usually a Google rate-limit block. Error rows are never charged; re-run just those domains.

n8n / Make / Zapier: Apify has first-party integrations for all three. Use the same POST for an enrichment step, or trigger on run completion and read the dataset for scheduled monitor runs.

This actor only reads data that Google publishes for public inspection in the Ads Transparency Center. Use it in line with Google's terms and applicable law; you are responsible for how you use the output.

Support

Open an issue on the actor page with the run ID and input. If Google changes the Transparency Center's data format, affected fields go null rather than failing the run; report which field and we will patch it. If the actor saved you time, a review helps other people find it.