Track Competitor Google Ad Launches and Flight Dates 2026
Use first and last shown dates from Google's Ads Transparency Center to detect competitor campaign launches, retirements and creative longevity automatically.

Thirdwatch's Google Ads Transparency Scraper returns
first_shownandlast_shownon every creative, which is enough to reconstruct a competitor's campaign calendar. Schedule the run, diff consecutive snapshots, and you get launch dates, retirement dates and creative longevity per advertiser without ever opening the Ads Transparency Center. Built for growth teams who need to know when a rival started spending, not just what they said.
Why track Google ad flight dates instead of ad copy
Knowing what a competitor says in an ad is worth less than knowing when they said it. Flight dates tell you about budget behaviour: when a campaign launched, how long a creative survived, whether a rival is in a seasonal push or a permanent always-on program. Alphabet's annual results put YouTube advertising alone at $36.1 billion for 2024, and video campaigns in particular run in defined flights rather than continuously. Missing the start of one is missing the campaign.
The Ads Transparency Center publishes exactly the two dates you need. Every creative in the archive carries the first date Google observed it serving and the most recent date it was seen. Those two numbers give you a flight window per creative. Aggregate them per advertiser and you get a campaign calendar; diff them across weeks and you get a launch and retirement feed.
What the Center will not give you is any of that in a usable shape. The interface shows one creative at a time behind a click. There is no way to sort by launch date, no way to ask which creatives went live this week, and no way to know that a creative you saw last month has since gone dark. Scraping the archive on a schedule turns two dates per creative into a proper time series.
How does this compare to the alternatives?
Flight-date tracking is either a scheduled data pipeline or a person with a calendar reminder. The difference shows up in coverage, not accuracy.
| Approach | Pricing | Reliability | Setup time | Maintenance |
|---|---|---|---|---|
| Manual weekly check in the Center | Free | Depends entirely on someone remembering | Zero | 45-90 min per week per competitor |
| DIY scraper plus your own scheduler | Free plus engineering time | Breaks when the archive changes shape | 3-5 days | Yours, permanently |
| Thirdwatch actor on an Apify schedule | Pay per result | Same named fields every run | Under 15 minutes | Thirdwatch tracks Google's changes |
Manual checking degrades quietly. It works for two competitors and collapses at ten, and the failure mode is not an error message, it is a gap in your history that you only notice when someone asks when a rival launched. A scheduled pull has no such failure mode, and the archive keeps the launch dates for you even if you start monitoring late.
How to track competitor ad launches in 5 steps
Step 1: How do I set up the token and a baseline run?
Sign in at apify.com, copy your token from Settings and then Integrations, and take a first snapshot.
export APIFY_TOKEN="apify_api_xxxxxxxxxxxxxxxx"import os
import json
import requests
from datetime import datetime, timezone
ACTOR = "thirdwatch~google-ads-transparency-scraper"
TOKEN = os.environ["APIFY_TOKEN"]
COMPETITORS = ["booking.com", "expedia.com", "hotels.com", "agoda.com"]
resp = requests.post(
f"https://api.apify.com/v2/acts/{ACTOR}/run-sync-get-dataset-items",
params={"token": TOKEN},
json={
"domainsOrAdvertisers": COMPETITORS,
"region": "US",
"maxResultsPerQuery": 300,
"proxyConfiguration": {"useApifyProxy": True},
},
timeout=900,
)
stamp = datetime.now(timezone.utc).strftime("%Y-%m-%d")
with open(f"gads_snapshot_{stamp}.json", "w") as fh:
json.dump(resp.json(), fh, indent=2)Store every snapshot. The pipeline is worth nothing on run one and compounds from run two onwards.
Step 2: How do I find creatives that launched this week?
Filter on first_shown rather than diffing snapshots. Launch detection does not need history because Google already stores the launch date.
from datetime import datetime, timedelta, timezone
rows = resp.json()
cutoff = datetime.now(timezone.utc) - timedelta(days=7)
new_launches = [
row
for row in rows
if row["first_shown"]
and datetime.fromisoformat(row["first_shown"]) >= cutoff
]
new_launches.sort(key=lambda r: r["first_shown"], reverse=True)
for row in new_launches[:15]:
print(
f"{row['first_shown'][:10]} {row['query']:16} "
f"{row['advertiser_name'][:28]:30} {row['format']}"
)This works on the very first run, which is the useful property. A brand that shows twelve creatives with a first_shown inside the last seven days has just started something.
Step 3: How do I measure creative longevity?
Longevity is last_shown minus first_shown, and it is the closest thing to a performance signal the archive offers.
from datetime import datetime
def flight_days(row):
if not row["first_shown"] or not row["last_shown"]:
return None
start = datetime.fromisoformat(row["first_shown"])
end = datetime.fromisoformat(row["last_shown"])
return (end - start).days
scored = [(flight_days(r), r) for r in rows]
scored = [(d, r) for d, r in scored if d is not None]
scored.sort(reverse=True, key=lambda pair: pair[0])
print("Longest-running creatives (proven performers):")
for days, row in scored[:10]:
print(f"{days:5}d {row['query']:16} {row['format']:6} {row['url']}")
print("\nShort flights (likely tests or seasonal):")
for days, row in scored[-10:]:
print(f"{days:5}d {row['query']:16} {row['format']:6} {row['url']}")A creative running for 1,400 days is a workhorse the competitor has never been able to beat. A cluster of creatives all running for under 30 days is a live testing program, and the ones that survive into the next quarter are the winners.
Step 4: How do I detect retirements between snapshots?
Retirement is the one thing you cannot see without history, so this step needs two snapshots.
import json
def load(path):
with open(path) as fh:
return json.load(fh)
def key(row):
return f"{row['advertiser_id']}/{row['creative_id']}"
old = {key(r): r for r in load("gads_snapshot_2026-09-01.json")}
new = {key(r): r for r in load("gads_snapshot_2026-09-08.json")}
retired = [old[k] for k in old.keys() - new.keys()]
launched = [new[k] for k in new.keys() - old.keys()]
print(f"launched: {len(launched)} retired: {len(retired)}")
for row in retired[:10]:
print(
f"RETIRED {row['query']:16} {row['format']:6} "
f"last seen {row['last_shown'][:10]}"
)advertiser_id plus creative_id is a stable composite key, which is why this diff is trustworthy where a text-based key would not be. Note that a creative can drop out of a capped result set without being retired, so keep maxResultsPerQuery constant across runs.
Step 5: How do I schedule it and get alerted?
Put the run on a cron and hang a webhook off the successful run.
curl -X POST "https://api.apify.com/v2/schedules?token=$APIFY_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "google-ads-flight-dates-weekly",
"cronExpression": "0 6 * * 1",
"timezone": "UTC",
"isEnabled": true,
"actions": [{
"type": "RUN_ACTOR",
"actorId": "thirdwatch~google-ads-transparency-scraper",
"runInput": {
"domainsOrAdvertisers": ["booking.com", "expedia.com", "agoda.com"],
"region": "US",
"maxResultsPerQuery": 300,
"proxyConfiguration": {"useApifyProxy": true}
}
}]
}'Wire an ACTOR.RUN.SUCCEEDED webhook through the Apify integrations API into a small handler that runs the diff from step 4 and posts new launches to Slack.
Sample output
Two real records from a booking.com pull, chosen to show the two ends of the longevity range:
[
{
"query": "booking.com",
"region": "US",
"advertiser_id": "AR02934798844673654785",
"advertiser_name": "Booking.com",
"advertiser_domain": "booking.com",
"creative_id": "CR08992408933561794561",
"format": "text",
"first_shown": "2021-10-28T07:00:00+00:00",
"last_shown": "2026-09-07T11:18:20+00:00",
"image_url": "https://tpc.googlesyndication.com/archive/simgad/8509128583226969181",
"advertiser_url": "https://adstransparency.google.com/advertiser/AR02934798844673654785?region=US",
"url": "https://adstransparency.google.com/advertiser/AR02934798844673654785/creative/CR08992408933561794561?region=US",
"source": "Google Ads Transparency Center"
},
{
"query": "hubspot.com",
"region": "US",
"advertiser_id": "AR14639398330518470657",
"advertiser_name": "Tested4you",
"advertiser_domain": "hubspot.com",
"creative_id": "CR14472003294416863233",
"format": "text",
"first_shown": "2026-06-02T12:04:04+00:00",
"last_shown": "2026-09-08T12:12:48+00:00",
"image_url": "https://tpc.googlesyndication.com/archive/simgad/9268441189215670650",
"advertiser_url": "https://adstransparency.google.com/advertiser/AR14639398330518470657?region=US",
"url": "https://adstransparency.google.com/advertiser/AR14639398330518470657/creative/CR14472003294416863233?region=US",
"source": "Google Ads Transparency Center"
}
]The first creative has been serving since October 2021 and was still live the day before the run: roughly 1,775 days of continuous flight, which is a proven asset, not an experiment. The second launched in June 2026 and is 98 days old, which puts it in the window where you should be watching whether it survives. Both carry a last_shown within a day of the run, so both are currently in rotation.
Common pitfalls
Reading last_shown as a retirement date on a live ad. For a serving creative, last_shown is simply the most recent observation and advances every day. It only becomes a retirement date once it stops moving, which you can only see across snapshots.
Changing maxResultsPerQuery mid-pipeline. Raising or lowering the cap changes which creatives fall inside the returned set, and your diff will report phantom launches and phantom retirements. Fix the cap on day one.
Changing the region mid-pipeline. The same applies to region, which filters on where creatives were shown. A switch from US to GB produces a completely different population and destroys comparability.
Trusting round timestamps. Older records frequently carry a first_shown of exactly 07:00:00 UTC, which is a date-level record rather than a precise moment. Treat pre-2023 launch dates as accurate to the day, not the hour.
Thirdwatch's actor normalises both dates to ISO 8601 UTC on every record, so your diff logic never has to parse a display format.
Related use cases
- Google Ads Transparency Scraper
- Scrape Google Ads Transparency Center for competitor ads
- Build a Google display ad creative swipe file
- Verify which advertisers run Google Ads on a domain
- Cross-channel ad intelligence with Google and Meta
- Monitor competitor Facebook ad campaigns
- The complete guide to scraping business data
- All Thirdwatch use-case guides
Frequently asked questions
What exactly do first_shown and last_shown mean?
They are the first and most recent dates Google observed that creative serving. first_shown is effectively the launch date. last_shown updates continuously while the creative is live, so a value from today means the ad is still in rotation right now.
How do I tell a live creative from a retired one?
Compare last_shown to your run time. Creatives still serving carry a last_shown within hours of the run. A last_shown that stops advancing across consecutive runs marks the week the competitor retired that creative.
Does creative longevity indicate performance?
Directionally, yes. Google publishes no spend or conversion data, but advertisers do not keep paying to serve a creative that loses money. A creative running for many months is a reasonable proxy for a proven performer.
How often should I run the pull?
Weekly suits most categories and keeps the time series clean. Move to daily for retail peaks, ticketed launches and travel sales, where a campaign can start and finish inside a single week and a weekly cadence misses it entirely.
Can I backfill history without running weekly for months?
Partly. first_shown gives you real launch dates stretching back years on the first run, so you can reconstruct a launch timeline immediately. Retirement dates only become visible once you have two or more snapshots to diff.
Related
100 free credits, no credit card.
About 30 real searches. Add the MCP to Claude or Cursor in two minutes.