Google Ad Copy Scraper — Ads Transparency Center, CTAs & OCR
Pricing
from $6.00 / 1,000 ad creative contents
Google Ad Copy Scraper — Ads Transparency Center, CTAs & OCR
Google Ads scraper for the Transparency Center: what competitor ads actually say — headline, body, CTA and all visible text from Ads Transparency Center creatives, image ads read by OCR, by advertiser or domain. No start fee. Charged only when ad content is extracted, never for unreadable creatives.
Pricing
from $6.00 / 1,000 ad creative contents
Rating
0.0
(0)
Developer
Steadyfetch Team
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
2 hours ago
Last modified
Categories
Share
Every Google Ads Transparency scraper returns the same thing: metadata rows — IDs, dates, formats — and stops. The Transparency Center renders every ad as a screenshot image, so the actual creative content (headline, body, CTA) never appears in their output. This actor reads it.
Using an AI agent? Pin this actor in Apify's MCP server with one link: https://mcp.apify.com?tools=steadyfetch/google-ads-creative-text-scraper
- Actor id:
steadyfetch/google-ads-creative-text-scraper - Input:
{ "advertisers": ["HelloFresh"] }— the one field you have to set. - Cap the bill: set
maxTotalChargeUsdon the run (a run option, not Actor input), e.g.0.50— the run stops when it reaches it.
Just want to see it work? Click Start with nothing set and the run reads up to 3 real creatives of one advertiser (HelloFresh, US), charged like any run at your plan's per-creative price. Put your own advertisers or domains in the input for your own run. Changed only a setting — the region, the formats, a cap, New creatives only, a watchlist name — and named nothing? Those settings are applied to the same sample and the run goes ahead, charged like any run, with one uncharged sample_note row saying which settings were yours; a cap smaller than the sample is honoured exactly, a bigger one never grows it.
Advertiser or domain in → structured ad copy out. Running the same advertisers again? A creative already delivered to your account is handed back from that run and not charged a second time — automatically, whichever advertiser lane lists it. Watching competitors over time? Name a watchlist and re-run on a schedule: with New creatives only you get only what is new (see Watch advertisers on a schedule).
{"advertiserName": "HelloFresh","format": "TEXT","headline": "Dinner just levelled up - Weeknights, upgraded","body": "Not sure what's for dinner? Get farm-fresh ingredients and easy recipes delivered.","cta": "Get Offer","displayUrl": "www.hellofresh.ca/","rawText": "HelloFresh www.hellofresh.ca/ Dinner just levelled up …","status": "extracted","firstShown": 20260214,"lastShown": 20260818}
Output
One JSON row per creative. status: "extracted" rows carry the ad copy and are the only rows charged; everything else is uncharged, with a statusReason saying why. Real rows from the live example run below (long text trimmed with … for the page):
| status | statusReason | advertiserName | format | headline | body | cta | displayUrl | firstShown | lastShown |
|---|---|---|---|---|---|---|---|---|---|
extracted | null | HelloFresh SE | TEXT | Schmeckt Deiner ganzen Familie | Weniger Einkaufsstress: Plane flexibel, wähle Rezepte und koche frisch zu Hause. | null | www.hellofresh.de/ | 20260820 | 20260826 |
not_extracted | no_screenshot | HelloFresh SE | IMAGE | null | null | null | 20260819 | 20260826 | |
not_extracted | video_metadata_only | HelloFresh SE | VIDEO | null | null | null | 20260604 | 20260826 | |
extracted | null | HelloFresh SE | IMAGE | HelloFresh is Hiring in Poland | Bring your flavour to HelloFresh — fresh roles now open in Poland! | Apply Now | careers.hellofresh.com | 20260402 | 20260826 |
The last row is an image ad: headline, body and CTA read straight off the rendered creative by OCR — the rows other Transparency Center scrapers leave blank. The two middle rows are uncharged.

Browse the full live example dataset → — real output from a verified run, including the uncharged rows.
Every row also carries charged — true only on rows that were actually billed, so you can reconcile the dataset against your invoice — plus advertiser, advertiserId, creativeId, rawText (all visible text, verbatim), textTruncated and screenshotUrl. textTruncated is false on almost every creative; on one carrying more copy than a single read returns it is true, the text ships cut short at about 3,500 characters, and the row's statusReason says so. Non-extracted rows use statusReason — video_metadata_only, all_video, no_screenshot, screenshot_unavailable (the archive no longer serves it), screenshot_fetch_failed (temporary — re-run to retry), ocr_no_text_found, ocr_unreliable (a garbled read is never billed — and a creative carrying more copy than our ad-text reader returns in one read lands here too, with a reason line naming our limit rather than calling the creative unreadable), screenshot_too_large (the screenshot is bigger than this actor downloads, or bigger than our ad-text reading service accepts in one request — one attempt, then an uncharged row), ocr_service_unavailable (our text-reading service was unavailable — that is on us, not you; every remaining creative and target still gets its own uncharged row saying so, and the run stops cleanly), no_creatives_found, advertiser_not_found, domain_not_listed, input_error (a value we could not place, such as an unsearchable region — the row names the field) — and none of them are charged. Rows a fresh run can genuinely recover (source weather, screenshot-host blips, an outage on our side) additionally carry retryable: true, so you can filter exactly what a re-run would fix.
Every row carries isNew (true for a creative your account had never answered before) and firstSeenAt (the moment it was first answered — this run's clock for a new creative, the original sighting for an old one). A creative you already had comes back with "repeat": true and "firstSeenRunId" naming the run that first delivered it, "charged": false, and a note saying so.
Watch advertisers on a schedule — pay only for new creatives
You never pay for the same creative twice (the default sample aside — see the end of this paragraph). Every run remembers the creatives it answered for your account — in a key-value store called google-ads-watch-account in your own Apify account — under every identity the creative has (its creative ID and its screenshot asset). Run the same advertisers, domains or chained rows again and the creatives you already have come back from the run that delivered them: repeat: true, firstSeenRunId, no screenshot fetched, no text read again, not charged. The status line counts them (2 of these you already had: handed back and not charged) and OUTPUT.repeats holds the number. Delete that store to forget everything; entries older than 90 days are no longer treated as repeats. The memory is a key-value store in your own Apify account, so a run started with a scoped API token in restricted-access mode needs key-value store Read, Write and Create permission (or Actor runs set to Full access) (Write is what lets the run remember what it delivered; without it every run pays again) — without it the run says so on its status line and in an uncharged note row at the top of the dataset, and it cannot skip what you already have. The one exception is the default sample — a Start with nothing set, or with only settings set: it always runs live, so it is neither read from nor written to that memory, and a second bare Start pays for its creatives again. Every run that names its own advertisers, domains, dataset or rows is covered exactly as above.
Competitor research is rarely a one-off: the question is usually what did they launch since last week? A watchlist answers that with only the new creatives.
- Put the brands or domains in Advertisers / Domains (a chained scraper run works too).
- Give the run a Watchlist name, e.g.
acme-competitors, and turn on New creatives only. - Save the input as a Task, put the Task on an Apify Schedule (daily works well for active advertisers), and add an integration or a webhook on run succeeded — a Google Sheet, Slack, n8n, Make, or your own endpoint.
{"advertisers": ["HelloFresh", "Factor"],"region": "US","maxCreativesPerAdvertiser": 30,"watchlistId": "acme-competitors","newCreativesOnly": true}
Each scheduled run then delivers only the creatives the list has not answered before, each with isNew: true and its firstSeenAt. Creatives already on the list are skipped before any screenshot is fetched — not fetched, not read, not charged — and they do not count against Max creatives per advertiser/domain, so the run walks past what you already have to the new ones. The run's status line says exactly what happened, for example: watchlist "acme-competitors": 4 new creatives this run, 61 already on the list were skipped (not fetched, not charged); 65 on the list now. The same numbers sit in the run's OUTPUT record under watchlist, newItems and skippedSeen.
Some details worth knowing:
- The list lives in your account, in a named key-value store called
google-ads-watch-<your name>(one record,SEEN). Open it in Storage → Key-value stores to see every creative the actor has answered under that name, or delete the record to start the list over. This actor runs with limited permissions and touches nothing else in your account. - Only answered creatives go on the list. An extracted creative counts, and so does a final uncharged verdict — a creative with no public screenshot, a video creative (listed uncharged here), a screenshot with no readable text. A screenshot the host did not serve this minute, or an outage on our side, is not remembered, so the next run tries it again.
- Leave New creatives only off and every creative runs as usual — you still get
isNew/firstSeenAton each row, which is the way to build a full "what is running now" picture while still knowing what is fresh. - New creatives only without a watchlist name compares against your account's memory instead: creatives already delivered to your account are skipped before any fetch. Only when neither a list nor the memory can be read does the run stop with one uncharged row, instead of charging you for creatives you may already have. A named watchlist it cannot open stops the run the same way, before any fetch. When the reason is a scoped API token with no key-value store permission, the row says exactly that and names the permission to grant.
- If your account's memory cannot be read, the run still runs: it delivers and charges as usual, and the status line and the charged rows say the repeat check was unavailable (
repeatCheck: "unavailable"). A scoped API token with no key-value store permission is called out by name — a re-run with the same token cannot fix it, so the run tells you to give the token key-value store Read, Write and Create permission (or set Actor runs to Full access) under Settings → API & Integrations, or to run from the console. - One creative, one sighting — the same creative listed under two advertiser accounts, or found by a name and by a domain, is one entry and one charge, and a creative the Transparency Center re-renders under a new screenshot address is recognised as the same creative.
For AI agents & LLMs — copy this block
Actor: steadyfetch/google-ads-creative-text-scraper (Apify Store)Does: Google Ads Transparency Center creatives -> what the ad actually SAYS:headline, body, CTA, display URL and all visible text. Image/display adsare read by a vision model (OCR). One JSON row per creative. No Google login.Run: POST https://api.apify.com/v2/acts/steadyfetch~google-ads-creative-text-scraper/run-sync-get-dataset-items?token=APIFY_TOKENInput (advertisers, domains, and/or a chained scraper run — at least one):{"advertisers": ["HelloFresh", "AR17410177287600472065"], // names or AR… advertiser IDs"domains": ["factormeals.com"], // every advertiser verified for it"datasetId": "7qBRP5ezeRo5gbeDZ", // optional: a finished Google Ads scraper run — each row's// advertiser ID / name / domain / ad-page link becomes a target"datasetItems": [{"advertiserName": "Nike"}], // optional: the same rows, pasted"watchlistId": "acme-competitors", // optional: remember answered creatives under this name (your own account)"newCreativesOnly": false, // with a watchlistId: skip creatives already on the list before any fetch"formats": ["TEXT", "IMAGE", "VIDEO"], // optional, this is the default"region": "US", // optional, default US"maxCreativesPerAdvertiser": 50, // optional, default 30; up to 500 per// advertiser/domain — ask for more and the run continues at 500"maxAdvertisersPerName": 1 // optional; >1 also opens the next-largest, up to 10 per name// same-name advertisers, ranked by ad count}Nulls every optional field above also accepts an explicit null, meaning "use thedefault" — so a template that renders unset fields as null (n8n, agentframeworks, chained workflows) runs exactly like one that omits them.Note: names are matched against ADVERTISER NAMES — the Transparency Centerhas no ad-text or topic search.Output one row per creative: status, charged, statusReason, advertiser, advertiserName,advertiserId, creativeId, format, headline, body, cta, displayUrl, rawText,textTruncated, screenshotUrl, firstShown, lastShownPricing pay per event, platform usage INCLUDED, no start fee:ad-creative-content $0.015 -> $0.006 per extracted creative (falls with your Apify plan tier)The paid floor has been $0.006 per extracted creative since 15 September 2026 20:15 UTC — a risenotified on 1 September; the Apify free-plan price stays $0.015.Charged only on extraction — video creatives, missing screenshots and unreadablecreatives ship as uncharged rows.Cap any run with maxTotalChargeUsd (minimum $0.05). Empty input runs a 3-creative sample,charged like any run. A creative your account already has is handed back uncharged.MCP: reachable through Apify's MCP server; ask for "what Nike's Google ads say".
What you get
- TEXT and IMAGE creatives read by a vision model — headline, body text, CTA, display URL, and all visible text verbatim. Image/display ads included: the format most tools skip entirely.
- Video creatives listed uncharged — advertiser, dates, format, creative ID as metadata rows. No video is downloaded and nothing is charged for them.
- Search by advertiser name, AR… advertiser ID, or domain (a domain spans every advertiser verified for it). Region-aware — a name resolves among advertisers registered in your
regionfirst, and the same brand runs different ads per country. - Charged only when content is extracted. A creative whose screenshot is missing, expired, or unreadable is delivered uncharged with an honest per-row reason (
screenshot_unavailable,ocr_no_text_found, …) — never a charged empty row.
Who uses this
- Competitive ad research: pull a competitor's live headlines and offers as text you can diff, count, and track over time.
- Ad copywriting / creative teams: mine what the market's ads say before writing yours — real inputs for ad-generation workflows.
- Agencies: monitor client categories across regions without screenshotting by hand.
How much does it cost?
1 result = one creative's extracted content (all fields above from one ad). No start fee, no subscription. Pricing is all-inclusive — platform usage is included in the event price, no separate usage line.
| What you run | Results | Cost |
|---|---|---|
| One competitor, 30 newest creatives | ~24 extracted | ~$0.36 |
| 10 competitors, 50 creatives each | ~400 extracted | ~$6.00 |
| Track one brand's ads weekly | ~100/mo | ~$1.50/mo |
Video creatives, misses, and unreadable screenshots are always $0. You stay in control with Maximum cost per run — the run stops cleanly at your cap and tells you what's left. Apify's free plan ($5 monthly credit) is enough to evaluate seriously.
A name matches advertiser names, not ad text. Google's Transparency Center has no ad-text or topic search, so "Advertisers" is matched against advertiser names — and a big brand shares its name with unrelated advertisers. Nike matches ten advertisers registered in the US alone, one of which runs ~9,000 ads and the rest a handful each. The name resolves to the one running the most ads in your region; set maxAdvertisersPerName above 1 to also cover the next-largest namesakes (largest first), and the status line names exactly which advertisers were opened and how many ads each has. Finding advertisers is never charged — but each opened advertiser gets its own maxCreativesPerAdvertiser allowance, so raising it raises what a run can cost. Both caps have limits — 10 advertisers per name, 500 creatives per advertiser or domain — and asking for a bigger number never stops the run: it goes ahead at the limit, with one uncharged row saying what you asked for and what was used.
Tip — search by domains for full coverage. Big brands often run ads under several verified advertisers (plus their agencies). A domain search finds every advertiser verified for that domain, so domains: ["brand.com"] regularly surfaces creatives — and whole advertiser accounts — that a name search misses. The counts in the table above assume the advertiser you hit actually runs text/image ads; if a name search comes back thin, try the domain.
Honest scope (read before big runs)
- v1 extracts TEXT and IMAGE creatives. Video creatives ship metadata only (uncharged rows) — no video content is fetched. An advertiser whose ads are all video gets those uncharged rows plus one uncharged row pointing you to the video-transcript companion — never an empty dataset.
- Region matters. An advertiser can show thousands of ads in one country and zero in another. Default is US; set
regionfor others. Ordinary spellings work (US,us,USA,United States). A region we cannot place is never swapped for another market — the run stops and returns uncharged rows naming the field, the searchable regions, and every target it did not search. - Some rich/HTML creatives have no public screenshot; those rows arrive uncharged with
no_screenshot. - The Transparency Center shows what Google publishes — typically ads served in roughly the last 30 days.
Input
{"advertisers": ["HelloFresh", "AR17410177287600472065"],"domains": ["factormeals.com"],"datasetId": "7qBRP5ezeRo5gbeDZ","formats": ["TEXT", "IMAGE", "VIDEO"],"region": "US","maxCreativesPerAdvertiser": 50}
Chain a scraper run. Put a finished Google Ads / Transparency Center scraper run's dataset ID in datasetId — Apify's Connect actor integration fills it in for you — or paste its rows into datasetItems. Each row's AR… advertiser ID, advertiser name, domain or adstransparency.google.com ad-page link becomes a target, one per row, with no re-searching and nothing paid twice. A dataset that can't be read, a row that names nothing, or a dataset bigger than one run can take is reported in an uncharged row that says exactly what happened — never a silent empty result.
Run with no input at all and you get a 3-creative sample of one real advertiser (HelloFresh, US), charged like any run.
Use it via API and MCP
curl "https://api.apify.com/v2/acts/steadyfetch~google-ads-creative-text-scraper/run-sync-get-dataset-items?token=$APIFY_TOKEN" \-X POST -H 'Content-Type: application/json' \-d '{"advertisers":["Nike"],"region":"US","maxCreativesPerAdvertiser":25}'
Works out of the box from any MCP client (Claude, Cursor, …) via Apify's MCP server — ask for "what do Nike's Google ads say right now" and point it at this actor.
FAQ
Why doesn't my usual Transparency scraper return the ad text? Because the Transparency Center serves ads as rendered screenshots — the text isn't in the page data. Reading it requires per-ad OCR, which row-priced scrapers don't do. That's the exact job this actor exists for.
What if an advertiser has no ads in my region? You get an uncharged row saying no_creatives_found with guidance to try another region — not a charged empty result.
Can it read every ad? No — and it won't pretend to. Creatives without a public screenshot or whose text can't be read reliably are delivered uncharged with the reason on the row. You pay only for extracted content.
Is this an official Google product? No. This is an independent actor from steadyfetch — unofficial, and not affiliated with, endorsed by, or sponsored by Google LLC. "Google" and "Google Ads" are trademarks of their respective owners, used here only to name the public Ads Transparency Center surface this actor reads.
Steadyfetch ad-intelligence suite
One actor per ad surface, one job each, the same contract everywhere: all-inclusive pay-per-event, no start fee, charged only on delivery — and the same row shape, so a workflow built for one platform ports to the next.
Ad creatives → transcripts & on-creative text
| Ad surface | Actor | Free n8n template |
|---|---|---|
| Facebook & Instagram Ad Library | Facebook Ads Transcript Scraper — Ad Library Video & Image Ads | Facebook ad transcripts · Competitor ad teardown |
| Google Ads Transparency Center | Google Ads Video Transcript Scraper — YouTube Ads to Text | Google Ads video transcripts |
| TikTok Creative Center Top Ads | TikTok Ads Transcript Scraper — Creative Center TikTok Top Ads | TikTok Top Ads transcripts |
| LinkedIn Ad Library | LinkedIn Ads Transcript Scraper — LinkedIn Ad Library to Text | LinkedIn Ad Library transcripts |
| Instagram Reels (organic posts) | Instagram Reels Transcript Scraper — Reels to Text, No Login | Instagram Reel transcripts |
Ad creatives → text (image & text ads, read by OCR)
| Ad surface | Actor |
|---|---|
| Google Ads Transparency Center | this actor |
Free n8n workflow templates for these actors are listed on our profile website (link on our Apify profile).
The rest of the steadyfetch shelf — same contract everywhere: all-inclusive pay per event, no start fee, charged only on delivery.
| Family | Actors |
|---|---|
| Ad creative intelligence | Facebook · Google Ads video · TikTok · LinkedIn · Google Ads text & OCR |
| Trends & keywords | Google Trends · Trends Now · Breakout keywords · Autocomplete keywords · Keyword volume & CPC · Social trends |
| YouTube transcripts | YouTube videos · YouTube channels |
| Reel transcripts · Profile posts | |
| Jobs | Indeed · Career sites by domain · Glassdoor · Multi-board · Google Jobs |
| Amazon | Products · Search · Bestsellers · Sellers |
| Any media file | Speech to Text · any link or file |
Feedback & support
Found an issue? Open it on the Issues tab — we usually reply within a couple of hours, always within a day. Feature requests welcome. If it earned its keep, a rating helps other buyers find it, and saving the actor keeps it one click away.