Google Ads Monitor — Competitor Ads, New Ads Only, Full Ad Copy
Pricing
from $0.70 / 1,000 ad returneds
Google Ads Monitor — Competitor Ads, New Ads Only, Full Ad Copy
Track competitors' Google ads from the Ads Transparency Center. Monitor mode returns only ads Google first showed since your last run. Text-ad copy read by OCR, video ad copy, landing URLs, regions. Flat schema, never null. Built for pipelines and AI agents.
Pricing
from $0.70 / 1,000 ad returneds
Rating
0.0
(0)
Developer
Sumitr Mardy
Maintained by CommunityActor stats
1
Bookmarked
2
Total users
1
Monthly active users
a day ago
Last modified
Categories
Share
Track your competitors' Google ads from the Google Ads Transparency Center. Give it domains or advertiser IDs, put it on a schedule, and each run returns only the ads Google first showed since the previous run — with the headline and description of text ads read for you.
One flat JSON record per ad. Every key is present on every record, and nothing is ever null.
Why this one
Most Google Ads scrapers are built to download an advertiser's archive once. Monitoring is a different job, and three things break when you try to do it with a one-shot scraper:
- "New" is decided by Google's own first-shown date, not by "an ID I haven't seen". The Transparency Center lists ads by when they were last shown, so old ads drift back into the first pages every time they run again. Measured overnight on 20 large advertisers: 2,401 ad IDs were unseen the day before, and 2 of them were actually new. A monitor that bills every unseen ID charges you for reruns of last year's ads. This one returns an ad only when its
firstShownfalls after your previous check. - Text-ad copy is included. About 75% of Google ads are text ads, and 96% of those exist in the archive only as a screenshot — there is no headline or description field in the data. This actor reads the screenshot (OCR) and returns
headline,descriptionanddisplayUrl, in the base price. Measured accuracy is below. Video ads come with their headline, description, call to action, landing URL and YouTube link too (92% of video ads). - You pay for the monitoring, not for re-reading the archive. A daily check of 50 competitors is 50 × $0.01 plus the genuinely new ads. No seed run, no per-run fee.
Status — what works and what does not
Stated up front, because you would find it anyway.
| Feature | Status |
|---|---|
| Domains, advertiser IDs, Transparency Center URLs as input | works |
Region filter (any ISO country code, or anywhere) | works |
Monitor mode (firstShown after the previous check) | works |
| All mode (newest-shown ads, up to a limit) | works |
| Format, first/last shown, days served, variations, served regions | works, every ad |
| Text-ad copy (headline, description, display URL) | works: 33/40 ads exact on both fields, headline 36/40, on two untouched test sets (details below) |
| Korean text-ad copy | weak: readable, but word spacing is wrong |
| Copy of video ads (headline, description, call to action, landing URL, YouTube link) | works: 92% of video ads with copy, 93% with a YouTube link, read from the ad's own data across all its variations. App-install videos give the app name and store link |
| Copy of image ads | partial: ~15%, only when the ad carries its own data; image-only banners have no text to read |
| Impressions per region, audience targeting | not extracted |
| Stopped / paused ads | not reported yet |
| Age-restricted ads | not visible (no login is used) |
Input
| Field | Type | Default | Description |
|---|---|---|---|
targets | string[] | — | One per line: a domain (hubspot.com), an advertiser ID (AR10072600183532683265), or a Transparency Center URL (advertiser, creative or domain search). Required. |
mode | monitor | all | monitor | monitor: only ads first shown since the last run for that target. all: every ad, newest-shown first, up to maxAdsPerTarget. |
region | string | anywhere | Two-letter country code to see only ads served there. A region inside a pasted URL wins over this. |
lookbackDays | integer 1–365 | 7 | How far back the first monitor run of a target reaches. Later runs start where the previous one ended. |
maxAdsPerTarget | integer | 100 | All mode only. Monitor mode returns every new ad. |
adCopy | boolean | true | Read ad copy (text ads by OCR, video and image ads from their own data). Turn off for faster runs when you only need IDs and dates. |
monitorName | string | default | Keeps separate schedules apart. A daily and a weekly schedule on the same targets need different names, or each sees the other's runs as its own. |
maxPagesPerCheck | integer | 100 | Safety cap on listing depth (40 ads per page). |
concurrency | integer 1–10 | 4 | Targets checked side by side. |
residentialFallback | boolean | true | A target that keeps getting blocked on your proxy switches to Apify residential for the rest of the run, at no extra charge to you. |
proxyConfiguration | object | Apify Proxy | Datacenter by default. |
{"targets": ["zoom.us", "monday.com", "AR10072600183532683265"],"mode": "monitor","region": "US","lookbackDays": 7}
Then schedule it (Apify Console → Schedules) daily or hourly with the same input.
A domain is a landing-domain search
Searching zoom.us finds every advertiser whose ads land on zoom.us, not only Zoom itself. Across 20 large brands the main advertiser held 83% of the creatives; the rest are resellers, agencies, affiliates and the brand's other legal entities (zoom.us: 37 advertisers, squarespace.com: 25). That is useful, because it catches every entity of a brand, but it is not filtered for you. Every record carries advertiserId and advertiserName, so you can keep only the brand, or pass the advertiser ID (AR…) instead of the domain.
Output
One record per ad. A real one, from a run on 2026-10-20:
{"target": "https://adstransparency.google.com/advertiser/AR10072600183532683265?region=anywhere","targetType": "advertiser","region": "anywhere","advertiserId": "AR10072600183532683265","advertiserName": "Hubspot, Inc.","creativeId": "CR02685094814793334785","adUrl": "https://adstransparency.google.com/advertiser/AR10072600183532683265/creative/CR02685094814793334785","format": "TEXT","landingDomain": "hubspot.com","firstShown": "2026-01-29T08:00:00.000Z","lastShown": "2026-10-20T05:32:10.000Z","daysServed": 264,"regions": ["US", "CA"],"variations": 1,"headline": "HubSpot for Startups - Bootstrap to Series A","description": "Partnered with top VCs and accelerators. Get exclusive startup pricing and resources. From...","displayUrl": "www.hubspot.com/","landingUrl": "","callToAction": "","videoUrl": "","copySource": "ocr","copyLanguage": "eng","previewImageUrl": "https://tpc.googlesyndication.com/archive/simgad/12284280382862371734","previewScriptUrl": "","checkedAt": "2026-10-20T06:48:02.980Z"}
The trailing "From..." is Google's own truncation in the archived ad, not an OCR cut. A
text ad read from its screenshot has no landing URL (the screenshot shows only the display
URL), so landingDomain comes from the display URL here. A video ad (hubspot.fr, 2026-10-20),
copy fields only:
{"format": "VIDEO","landingDomain": "hubspot.fr","headline": "CRM de HubSpot","description": "Configurez votre CRM en quelques minutes pour accélérer votre activité dès aujourd'hui.","displayUrl": "hubspot.fr","landingUrl": "https://www.hubspot.fr/crm/e010a-7-0","callToAction": "En savoir plus","videoUrl": "https://www.youtube.com/watch?v=wTEjgF1qocM","copySource": "contentjs"}
Field guarantees
Every key is present on every record. Strings are '' when empty, arrays are [], numbers are 0. Never null, so a pipeline can read any field without a guard.
| key | type | notes |
|---|---|---|
target, targetType | string | the input as given; domain or advertiser |
region | string | ISO alpha-2 filter used, or anywhere |
advertiserId, advertiserName, creativeId, adUrl | string | |
format | string | TEXT, IMAGE, VIDEO, or UNKNOWN |
landingDomain | string | the searched domain for domain targets; for advertiser targets, taken from the ad's landing URL, else its display URL; '' when the ad shows neither |
firstShown, lastShown | string | ISO 8601 |
daysServed, variations | number | |
regions | string[] | ISO alpha-2 codes where it was served |
headline, description, displayUrl | string | '' when there is no copy |
landingUrl, callToAction | string | from the ad's own data (video and some image ads); '' otherwise |
videoUrl | string | YouTube link for video ads; '' otherwise |
copySource | string | ocr, contentjs or none |
copyLanguage | string | OCR language pack that read it, or '' |
previewImageUrl, previewScriptUrl | string | one is usually '' |
checkedAt | string | ISO 8601 |
Each run also writes a SUMMARY record to its key-value store: per target, its status (ok, failed, partial_cost_limit, …), pages read, ads scanned and returned, and whether the listing cap was hit (truncated).
How monitor mode works
For each target, state is kept per monitorName + target + region:
- The check starts at the previous check's time. On a target's first run it starts
lookbackDaysago. - The listing is read newest-shown first, and stops at the first page that reaches back past that time — nothing shown before it can be new.
- An ad is new when Google's
firstShownis after the start (with a 2-day margin, because the archive publishes ads up to about a day late) and it has not been returned before. The margin never bills an ad twice. - Only new ads are looked up, read and returned.
- The state moves forward only after the ads are delivered and charged. A target that failed or hit your cost limit keeps its old state, so the next run returns what this one could not.
If every target fails — Google blocking, or the archive changing — the run ends FAILED with the cause. It never hands back an empty dataset that would look like "no new ads".
How many new ads to expect: it varies a lot by advertiser. Measured on large brands, from 0 per day (booking.com, expedia.com) to more than 17 per day (monday.com: 157 in 9 days).
Text-ad copy: how it is read, and how accurate it is
Google renders each part of a search ad in a fixed colour — the headline blue, the URL and description grey. The actor reads the archived screenshot with Tesseract, assigns each word a field by its colour, and picks the OCR language from the countries the ad was served in (an ad served only in Japan is read with Japanese and English). Ads delivered as script rather than image (about 4% of text ads) are parsed directly, with copySource: "contentjs".
Measured on two sets of 20 hand-transcribed ads, from 20 large advertisers, that the rules were never tuned on. The second set was drawn later from live ads, with more photo layouts and non-English copy, and it scored lower. Both numbers are shown:
| set 1 | set 2 | together | |
|---|---|---|---|
| headline and description exact | 19 / 20 | 14 / 20 | 33 / 40 (83%) |
| headline exact | 20 / 20 | 16 / 20 | 36 / 40 (90%) |
| character error rate, headline / description | 0.0% / 0.2% | 0.6% / 1.2% |
A miss is usually one glyph or a clipped word, not a wrong ad: the character error rate stays around 1%. Known misses, all measured:
- Korean: the Korean model splits words by syllable, so spacing comes out wrong. The words are readable; exact-match fails.
- Accents on ads served in many countries: an ad served in more than two language markets is read with English, so a Romanian ad served across Europe comes back as "Automata" instead of "Automată".
- Text cut by a photo: when an image overlaps the copy in Google's own archive render, the hidden words are not in the source at all.
- Single glyphs: an occasional misread or dropped character ("–" read as "—", "AEO" as "AEOQ", a dropped " — ").
For comparison, a general OCR pass over the same 20 ads in set 2 reproduced the headline exactly on 12 and the description on 3, because it reads the whole card (advertiser name, sitelinks, ratings) as one block of text. Splitting the fields by colour is what makes the copy usable as data.
copyLanguage tells you which language pack produced the copy, so you can treat non-English reads with appropriate care.
Pricing
Pay per event. No setup run, and no per-run fee of ours. Apify's standard start event applies ($0.00005 per run per GB of memory, so $0.00005 at the default 1 GB); in return Apify covers the first 5 seconds of compute.
| Event | Price | Charged when |
|---|---|---|
ad | $1.20 / 1,000 (Free) → $0.90 Bronze → $0.80 Silver → $0.70 Gold | each ad returned, copy included. In monitor mode only new ads are returned |
target_checked | $0.01 | each domain or advertiser checked in a monitor-mode run, whether or not anything changed |
Example: 50 competitors checked daily for 30 days is 1,500 checks ($15) plus the new ads they launch. All mode charges ad only.
The residential fallback is included: when it kicks in, the proxy traffic is paid by the actor, not added to your bill. Set a maximum cost per run in Apify Console and the actor stops cleanly at it — it never pushes ads it cannot charge for, and the monitor state is kept so the next run picks up the rest.
Known limits
- The Transparency Center is not an official API. The actor reads the same endpoints the website uses. If Google changes them, the run fails loudly with "the RPC may have changed" rather than returning partial data.
- Google blocks busy IPs. Checks run on your Apify datacenter proxy first. A target blocked twice in a row switches to residential for the rest of the run. Apify's Free plan has only 5 shared datacenter IPs, so expect the fallback to be used there.
- Big advertisers are expensive to read, not to bill. The listing has no "first shown" sort, so a check reads every ad shown since the last check: 20 pages for zapier.com, about 150 for salesforce.com over one day.
maxPagesPerCheck(100) caps it; a capped target is markedtruncatedinSUMMARY. Checking big brands more often keeps each check short. - The archive is up to about a day behind the live ads. An ad shown this morning may appear in tomorrow's run.
- Copy accuracy is 83% exact on both fields over 40 ads (above), with about 1% of characters wrong. Korean and accented copy in multi-country ads are the weak spots.
- Image-only banners have no copy. Most image ads are a bare picture with no text data behind them (copy found for about 15%). Their
previewImageUrlis given. - Speed: about a second per ad when reading copy at the default 1 GB. Give the run more memory to go faster, or turn
adCopyoff when you only need IDs and dates. - Not included: impressions, audience targeting, stopped-ad reports, age-restricted ads.
Use it from code
One call, results back (runs up to 300 seconds; keep maxAdsPerTarget small, or use the async pattern below):
curl -X POST "https://api.apify.com/v2/acts/sumitr_mardy~google-ads-monitor/run-sync-get-dataset-items" \-H "Authorization: Bearer YOUR_APIFY_TOKEN" \-H "Content-Type: application/json" \-d '{"targets": ["hubspot.com"], "mode": "all", "maxAdsPerTarget": 20}'
Node, any size:
import { ApifyClient } from 'apify-client';const client = new ApifyClient({ token: 'YOUR_APIFY_TOKEN' });const run = await client.actor('sumitr_mardy/google-ads-monitor').call({targets: ['zoom.us', 'hubspot.com'],mode: 'monitor',region: 'US',});const { items } = await client.dataset(run.defaultDatasetId).listItems();// Keep only the brand itself, not resellers landing on its domain:const ownAds = items.filter((ad) => ad.advertiserName.startsWith('Zoom'));for (const ad of ownAds) console.log(ad.firstShown, ad.format, ad.headline, ad.adUrl);
Daily competitor alerts: create a Schedule in Apify Console (daily, or hourly for fast movers) with mode: "monitor", and connect the run to Slack, Google Sheets, n8n, Make or a webhook. Each run delivers only the ads that are new since the last one, so the alert is the dataset itself.
Use it from AI agents (MCP)
This Actor is a tool on the Apify MCP server, so Claude, Cursor, VS Code or any MCP client can call it. Load only this tool: a short tool list keeps the agent from picking the wrong one.
Recommended: hosted server, OAuth sign-in (nothing secret on disk). Add to your MCP client config (claude_desktop_config.json, .cursor/mcp.json, …):
{"mcpServers": {"google-ads-monitor": {"url": "https://mcp.apify.com?tools=sumitr_mardy/google-ads-monitor"}}}
With an API token instead (clients without OAuth, or unattended setups):
{"mcpServers": {"google-ads-monitor": {"url": "https://mcp.apify.com?tools=sumitr_mardy/google-ads-monitor","headers": { "Authorization": "Bearer YOUR_APIFY_TOKEN" }}}}
Local server (stdio):
{"mcpServers": {"google-ads-monitor": {"command": "npx","args": ["-y", "@apify/actors-mcp-server@latest", "--tools", "sumitr_mardy/google-ads-monitor"],"env": { "APIFY_TOKEN": "YOUR_APIFY_TOKEN" }}}}
The hosted server also gives the agent this Actor's output schema, so it knows every field before the first call. apify mcp install <client> writes the config for you if you use the Apify CLI. Get a token at Apify Console → API & Integrations, and never commit it.
Tips for agents:
- "What ads is X running right now?" Use
mode: "all"withmaxAdsPerTargetof 10–25. Reading ad copy takes about a second per ad. - "What did X launch this week?" Use
mode: "monitor"withlookbackDays: 7. Calling it again later returns only what is new since then. - To filter to the brand, pass its advertiser ID (
AR…) instead of the domain, or filter byadvertiserName. - The MCP
call-actortool waits up to 45 seconds. A longer run keeps going: fetch the results withget-dataset-itemsand the run'sdatasetId. - Every record has the same keys and never a
null, so an agent can readheadline,firstShownorlandingUrlwithout a guard.copySource: "none"means the ad really has no text, such as an image-only banner. It does not mean the read failed.