# Google Ads Transparency Monitor: New Competitor Ads (`domestic_buffalograss/competitor-ad-alerts`) Actor

Get only your competitors' NEW ads from Google Ads Transparency Center as a daily Slack/webhook digest per competitor and client. Flags launches and new countries. $0.01 per run + $0.01 per competitor check + $0.005 per new ad. First check: free 30-day baseline summary. Not affiliated with Google.

- **URL**: https://apify.com/domestic_buffalograss/competitor-ad-alerts.md
- **Developed by:** [Sumitsubo](https://apify.com/domestic_buffalograss) (community)
- **Categories:** Lead generation, Marketing, Automation
- **Stats:** 1 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $7.00 / 1,000 competitor checks

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Google Ads Transparency Monitor: New Competitor Ads

**Get a Slack or webhook digest of your competitors' NEW ads from Google Ads Transparency Center - nothing you've already seen.**
Independent tool, not affiliated with or endorsed by Google. It reads only publicly available ad library pages and does not log in.
Pricing: **$0.01 per run** (run start) + **$0.01 per competitor check** + **$0.005 per new ad**; very large advertisers add $0.0005 per 40 ads read beyond 200 (the ads read in each country, added up over its countries). A day without new ads costs $0.01 + $0.01 per competitor checked, plus large-inventory pages when a competitor's read passes 200 ads, added up over its countries (each run reads the ads shown since the previous check plus the overlap, in every country; an ad shown in 3 countries counts 3 times). The first check of a competitor records a 30-day baseline: the **baseline summary is free**, the check itself is billed as usual (plus large-inventory pages for very large advertisers).
Enter competitor domains, press Start, see results in about a minute. Then schedule it daily.

- Watches [Google Ads Transparency Center](https://adstransparency.google.com/) by domain or advertiser ID, per country.
- Reports only ads it has not reported before, grouped per competitor (and per client, if you run an agency).
- Flags **campaign launches** (many new ads in 7 days), **new countries** (a watched country the competitor was not seen advertising in before, including known ads spreading to it) and **new ad formats**.
- Reads each competitor's list newest first and stops at the previous check (minus an overlap, default 48 hours, so ads that show up late in the Transparency Center are still caught). Competitors with many ads shown since then are read deeper, up to your read limit; their recently shown ads are read again on every run. Your memory of seen ads stays in your own Apify storage.

### What you get

**Slack digest** (one message per run):

```
Competitor ad alerts: 7 new ads across 2 competitors
Client: Acme
Example Brand · Google (GB, US)
  ◆ Campaign launch: 6 new ads in 7 days
  6 new · about 100-200 ads live
  • new · IMAGE · first shown 2026-10-10 · US      (links to the ad)
  • new · VIDEO · first shown 2026-10-10 · US,GB
  ...
Other Brand · Google (DE)
  1 new
  • back again · VIDEO · first shown 2026-09-28 · DE
No new ads: Brand C, Brand D
Run ... · full rows in the dataset · dates in UTC
```

Competitors with news are listed first (in every client they belong to); competitors with nothing to say are folded into one line per client and reason ("No new ads: ...", "Skipped (checked recently): ...", "Not checked (maximum charge reached): ..."), so a long watchlist does not push the news out of the message. The header counts only the competitors that were actually checked; if none could be checked it says so. English digests show dates in UTC, Japanese digests in Japan time.

**Dataset row** (one per new ad, `type: "new_ad"`):

```json
{
  "type": "new_ad", "platform": "google", "category": "new_launch",
  "label": "Example Brand", "client": "Acme", "advertiserName": "Example Brand Inc.",
  "adId": "CR00000000000000000000", "format": "IMAGE",
  "firstShownAt": "2026-10-10T07:12:00.000Z", "lastShownAt": "2026-10-11T05:40:00.000Z",
  "regions": ["US"], "previewUrl": "https://adstransparency.google.com/advertiser/AR.../creative/CR...?region=US",
  "previewImageUrl": "https://tpc.googlesyndication.com/simgad/...", "detectedAt": "2026-10-11T06:00:00.000Z"
}
```

Each run also writes one free `type: "advertiser_summary"` row per competitor with its status, counts, signals and (on the first check) the baseline summary. On a competitor's first check, the 5 latest ads of the baseline are also written as free `type: "baseline_sample"` rows, so the **New ads** view shows what a check returns before the first real new ad arrives. Filter on `type` in n8n/Make/Zapier if you only want new ads. The dataset has two views: **New ads** and **Per-competitor status**.

### Input

```json
{
  "watchlist": [
    { "label": "Patagonia", "client": "Acme", "googleDomain": "patagonia.com", "regions": ["US", "GB"] },
    { "label": "Competitor DE", "googleAdvertiserId": "ARxxxxxxxx", "regions": ["DE"] }
  ],
  "slackWebhookUrl": "https://hooks.slack.com/services/...",
  "maxChargeUsd": 1,
  "language": "en"
}
```

| Field | What it does |
|---|---|
| `label` / `client` | Your names. The digest is grouped by `client`. |
| `googleDomain` | The advertiser's website domain as shown in the Transparency Center (e.g. `patagonia.com`, not "Patagonia"). A Transparency Center advertiser URL pasted here is used as the advertiser ID. |
| `googleAdvertiserId` | More precise: the `AR...` id, or paste the Transparency Center advertiser URL. Recommended when a domain returns nothing or too much. |
| `regions` | Countries to check (ISO codes like `US`, `GB`, `JP`, or `anywhere`; `UK` is read as `GB`). A list, or a comma-separated text such as `"US, GB"`. Default `US`. An empty entry is an error. One check covers all listed countries; at most 20 countries per competitor (also when it is listed on several entries). |
| `slackWebhookUrl`, `webhookUrl` | Optional, stored as secrets. Slack: an Incoming Webhook URL (`https://hooks.slack.com/services/...`). A URL that cannot receive the digest as written (a Slack workflow or trigger URL, a webhook that is not https, an internal address) is refused before anything is read or charged: the run ends as aborted with "Invalid input" (the URL itself is not shown). A Slack message holds at most 50 blocks and about 40,000 characters; competitors beyond that are counted as "+N more competitor entries in the dataset". Without them, results are in the dataset and the run log. A failed delivery is re-sent to the same URL with the next run as a summary (new-ad counts, signals and the top 5 ads per competitor; the full rows are in the dataset). A summary too long for one Slack message is spread over the following messages, each competitor once. Webhooks must be public https addresses; redirects are not followed; each delivery carries an `idempotencyKey`. A webhook body is kept under 1 MB: above that each competitor lists its top 5 new ads (with the full count), and competitors or carried runs that still do not fit are left out and counted in `truncated`, which points to the dataset (every row is there). |
| `maxChargeUsd` | Default $1. The run stops between competitors before its charges would exceed this; competitors are taken least recently checked first, so the ones left out go first next run. A competitor that alone does not fit in what is left (a very large inventory) is listed as not checked and the run goes on with the others. A competitor that was read but could not be paid for goes after the others next run, and while what is left of the cap stays below its last read it is not read again (for up to 7 days; the estimate is at most what the current read limit allows, and is dropped when its countries change). The run reports the amount that would cover every competitor (run start, checks, the large-inventory pages of the competitors read, and the new ads seen). The lower of this and the platform's "Maximum cost per run" applies. For example 120 competitors cost about $1.21 per run in checks alone; the run warns when the checks exceed the cap. |
| `maxNewAdsPerAdvertiser` | Default 50. Beyond that, new ads are counted but not listed or billed. |
| `minHoursBetweenChecks` | Default 1. A competitor whose last check was in a run that started less than about this many hours ago (10% slack for schedule jitter) is skipped (not read, not billed). 0 turns it off. |
| `maxRunMinutes` | Default 30, at most 120. No new competitor is started after this (or 2 minutes before the platform timeout); the rest go first next run. |
| `stateKey` | Optional name of the seen-ads memory. In a task, leave it empty: each task gets its own memory automatically. **Outside a task (API calls, a schedule that starts the Actor itself), an empty state key uses the shared memory `main`.** The memory is kept per competitor, so editing the watchlist keeps it: an added competitor starts with a free baseline, a removed one is simply no longer checked (added back after a long pause, it gets a fresh free baseline). **To run a different watchlist outside a task, give it its own `stateKey`**: two watchlists on one key share one memory, so one would miss the new ads the other already saw. The run log and status message warn when one key is used with different Slack/webhook destinations. |

The same competitor listed for two clients is checked and billed **once** per run, shown under both clients, and read in the union of the countries of both entries (so client A may also see ads from client B's countries). If you enter the same Google advertiser once by domain and once by advertiser ID, both are read, but when both reads return the same ads (or both return nothing and memory already knows they are the same advertiser) only one check is billed and each new ad is billed once.

### Pricing (pay per event)

| Event | Price | When |
|---|---:|---|
| Run start | $0.01 | once per run that delivers a result |
| Competitor check | $0.01 | one competitor read: all its countries, or as many as the read limit allowed (the unread countries are read first next run) |
| Large inventory page | $0.0005 | per 40 ads read beyond the first 200 for one competitor (the ads read in each country, added up over its countries) |
| New ad | $0.005 | per new ad delivered |
| Baseline row | $0.001 | only if you turn on "Write baseline ads as dataset rows" |

Free: the first-check baseline summary (counts, countries, formats, latest 10) and its 5 sample rows (the first check itself is billed like any check), the digest, launch/new-country/new-format signals, quiet-day status.
Not billed: competitors that failed or were read only partly, competitors skipped as recently checked, and a competitor read a second time in the same run.
Competitor checks and new ads are discounted on higher Apify plans (for example $0.008 per check and $0.004 per new ad on SILVER). When `maxChargeUsd` (or the platform's maximum cost) is reached, the run stops between competitors before reading the next one, lists the rest as "not checked", and keeps any unpaid new ads for the next run; a single competitor too large for what is left is listed as "not checked" while the others are still checked. `maxChargeUsd: 0` reads nothing.

Example monthly cost: 5 competitors, daily, ~60 new ads/month - about **$2** (30 x ($0.01 + 5 x $0.01) + 60 x $0.005). 20 competitors (4 clients x 5) daily with ~200 new ads/month - about **$7.30**.

### What counts as "new"

| Category | Meaning | Billed |
|---|---|---|
| `new_launch` | First shown after the previous check minus the overlap (and inside the baseline window minus the overlap, and not before the window of the country's first check) | yes |
| `reactivated` | Not seen before, first shown earlier but inside that window, shown again after the previous check | yes |
| `first_seen_old` | First check of a competitor or country (the baseline); an ad first shown before the window (or before the window of the country's first check); an ad the previous check did not reach because of the read limit | no |

- Ads already reported are not reported again. The memory keeps every ad first shown in the last 90 days (up to 20,000 per competitor; beyond that the oldest are dropped, and an unseen ad first shown before the newest dropped one is not billed), plus up to 5,000 older ones per competitor (shown in the last 180 days).
- A competitor or country not checked for clearly longer than the baseline window (the window plus the larger of the overlap and a quarter of the window - for the default 30 days, 37.5 days) gets a fresh free baseline instead of a bill for everything that appeared meanwhile. A schedule only slightly slower than the window (for example weekly with `baselineDays: 7`) keeps billing new ads normally.

### Zero results vs. a failed check

- **`no_ads`** - the check worked and the competitor has no ads for that domain/country. Check the domain, or use the advertiser ID.
- **`failed` / `partial`** - the read was blocked or interrupted. Nothing is billed for that competitor and its memory is **not** moved forward, so the next run picks up everything it missed. When a country is blocked (HTTP 429/403/5xx, timeouts or network errors), the competitor's other countries are not read in that run. If 3 competitors in a row cannot be read for reasons outside the request (HTTP 429/403/5xx, timeouts or network errors), the run stops reading and lists the rest as `not_checked_rate_limited`. A competitor that could not be read goes after the others next run (it is still tried every run), so competitors that fail every time do not stop the run before the others are checked.
- **`truncated`** - the competitor's ad list is deeper than "Max ads read per competitor". The ads most recently shown are covered; countries the limit left unread are read first next run.
- **`skipped_recent`** - checked recently (see `minHoursBetweenChecks`); not a gap.
- **`not_checked_*`** - the run reached its maximum charge or time limit, or stopped for another listed reason. These competitors go first next run (a competitor that was read but could not be paid for goes after them).

If every competitor that was due fails, the run ends as **aborted** (nothing charged, memory unchanged) so your schedule shows it. A failed Slack/webhook delivery does not stop the run: it ends as **succeeded** after everything was checked, written and charged; the new ads are in the dataset, the status message starts with "Slack notification FAILED (...): sent again with the next run" (or "Webhook ..."), `RUN_SUMMARY` has `notificationFailed: true` and the same words in `warnings`, and the digest is re-sent to the same URL with the next run. A digest with no new ads and no signals (a first check, a quiet day) has nothing worth re-sending: its status message says "no new ads or signals in it, nothing to send again" instead. Temporary failures (timeouts, network errors, HTTP 408, 429 and 5xx) are retried once and then re-sent with the next run, however many times they happen. When the destination refuses it 3 runs in a row with a permanent answer (HTTP 3xx/4xx other than 408/429, such as 404 or 410 or a redirect, or an address refused at connect time), that run and every next run it refuses ends as **aborted**, and the status message says to check the URL in the input (`RUN_SUMMARY.checkDestination`); the 1st and 2nd refusal end as succeeded with the failure shown first, and one successful delivery resets the count. Checks and new ads are still charged while a destination fails. If a run is stopped half-way (timeout, crash, migration), the competitors it already delivered are notified by its restart or by the next run, and are not read or charged again.

### Scheduling and integrations

Create a task with your watchlist and a schedule (daily is typical). Each task keeps its own memory. Runs without a task (API, or a schedule that points at the Actor) share the memory `main` unless `stateKey` is set: give each different watchlist its own `stateKey`. For n8n, Make or Zapier, either point `webhookUrl` at your workflow, or use the Apify integration and read the dataset items with `type = "new_ad"`. If a run finds its memory still in use by an earlier run, it waits up to 5 minutes and then ends as aborted without checking or charging.

### Known limits

- **Opening the dataset in a spreadsheet?** Turn on **Spreadsheet-safe text** (`spreadsheetSafeText`): text that starts with `=`, `+`, `-` or `@` (advertiser names are written by the advertiser) gets a leading apostrophe, so it is shown as text instead of being run as a formula. It is off by default because it changes those values for JSON consumers.
- **Proxy: Residential** (the default). Google Ads Transparency Center answers datacenter proxy IPs with HTTP 429 on every request, so Apify Proxy without a group is switched to Residential (the run warns). Proxy costs are included in the event prices.
- **Shortest recommended schedule: once an hour** (daily is typical). A run started sooner than `minHoursBetweenChecks` (default 1 hour) after the previous one skips every competitor (nothing read, nothing charged), but every start still takes a few seconds of compute; a schedule every minute mostly starts runs that do nothing.
- A webhook body is kept under 1 MB and a Slack message under 50 blocks and about 40,000 characters; what does not fit is counted and is in the dataset.
- While a destination keeps failing, checks and new ads are still charged (the new ads are in the dataset) and up to 10 undelivered digests are kept for the next send. A failed delivery is shown first in the status message of a succeeded run; only a destination that refused 3 runs in a row with a permanent answer ends the run as aborted. A digest the destination refuses for its content (HTTP 400/413/422) stays among them and is sent again each run until it is pushed out.
- Setups that share one state key with different destinations: new ads of a run that was stopped before its digest are sent only by a run with that run's destination, or by a run that watches the same competitor under the same label and client. The rest are held for another destination: `RUN_SUMMARY.warnings`, `heldForOtherDestination` and the status message say "run X: N new ad(s) held for another destination" (they are in that run's dataset). N counts only the competitors the reporting run can see (those of its own watchlist, and entries kept in the run journal); new ads of competitors its watchlist does not have are not counted there. At most 3 undelivered runs are kept; one dropped with something left is said in `warnings`, `carryDropped` and the status message, again by the following runs until a run that said it has finished its digest (runs with nothing left go first).
- Changing the Slack/webhook URL: what is still undelivered for the old URL - a run stopped before its digest, and the digests kept in the old URL's outbox after failed deliveries - is sent to the new URL for the competitors the watchlist still has under the same label and client. The rest (a competitor removed or relabelled) is not sent and is said in `warnings` and the status message (they are in that run's dataset): "held for another destination" (`heldForOtherDestination`) for a stopped run, "run X: N new ad(s) not sent, held for a previous destination" (`heldForPreviousDestination`) for an old outbox; it is sent only if a run uses the old URL on this state key again. An old outbox is moved only by a run with the same competitors as the run that used it last (so another setup sharing the state key does not take it): change the URL in one edit and the competitors in another, or its digests stay held. Up to 5 earlier URLs per kind are looked at; outboxes left by v0.3.6 and older are not (their URL was not recorded).
- Two setups on one state key that watch the same competitor under the same label and client (for example both without a client) are treated as one setup: new ads one of them paid for, in a run stopped before its digest or left in its outbox, can be sent to the other's destination, and the setup that paid is not told. Give each setup its own state key.
- A run stopped after a successful delivery but before it cleared its outbox and run journal has that digest sent again by the next run (a duplicate notification; nothing is charged twice). The copy carries the same run id: in the webhook body it is in `carriedOver[].runId` (the original had it as `runId`).

### Need every ad, not just the new ones?

This Actor watches and alerts. For full exports, creative details and image archiving from the Transparency Center, the separate **Google Ads Transparency Scraper** Actor (same author, priced separately) reads complete ad lists.

Only Google Ads Transparency Center is covered (not Meta or other ad libraries).

### 日本語

競合の新着広告だけを毎日 Slack・Webhook に。Google 広告透明性センター（公開ページのみ・ログインなし）を確認します。Google とは無関係の独立したツールです。実行の開始 $0.01、競合の確認 1 件 $0.01、新着 1 件 $0.005。初回の 30 日分のベースライン要約は無料（初回も確認料はかかり、広告の非常に多い競合は大量在庫の読み込み料もかかります）。
`language` を `ja` にすると Slack のまとめが日本語になり、日付は日本時間で表示します。0 件（`no_ads`）と取得失敗（`failed`／`partial`）は区別して表示し、失敗した競合は課金せず既読も進めません（次回に取り直します）。1 回の実行の課金上限は `maxChargeUsd`（既定 $1）で、上限で確認できなかった競合は次回に先に確認します（読めなかった競合と、読んだのに上限で払えなかった競合は後ろに回します）。タスクごとに既読は別々に保存されます。タスクを使わずに実行する（API・Actor を直接起動するスケジュール）と、`stateKey` が空欄なら共有の既読 `main` を使います。既読は競合ごとなので、ウォッチリストを編集しても続きます（足した競合は無料のベースラインから、外した競合は確認しないだけ）。タスクの外で別のウォッチリストを回すときは、それぞれに別の `stateKey` を指定してください（同じキーに別の送信先の設定が来るとログと状態メッセージで警告します）。Slack・Webhook の URL が形式の時点で届かないもの（Slack のワークフロー URL・https でない・内部アドレス）は、読む前・課金の前に入力エラーで止めます。通知の送信に失敗しても実行は止めず succeeded で終わり、状態メッセージの先頭に失敗した通知と「次回に同じ URL へ再送」を示します（新着も出稿の動きも無い回は再送するものが無いので、その旨を示します。`RUN_SUMMARY` の `notificationFailed`・`warnings` にも残します）。一時的な失敗（タイムアウト・ネットワーク・HTTP 408／429／5xx）は何回続いても succeeded です。404 などの恒久的なエラーが 3 回続いた実行からは aborted で終わり、状態メッセージで URL の確認を促します（1・2 回目は succeeded。1 回でも届けば数え直し）。送信先の URL を変えると、古い URL 向けに残っていた未通知分（途中で止まった run の分と、送信失敗で送信待ち箱に残った分）は、同じラベル・同じクライアントで今も見ている競合の分だけ新しい URL に届きます。届かない分は状態メッセージと `RUN_SUMMARY.warnings` で件数を示します（新着はその run のデータセットにあります）。Webhook の本文は 1 MB 以内に収めます。スケジュールの最短の推奨間隔は 1 時間です。

# Actor input Schema

## `watchlist` (type: `array`):

One object per competitor. Fields: <code>label</code> (your name for it), optional <code>client</code> (groups the digest), <code>googleDomain</code> (e.g. patagonia.com; a Transparency Center advertiser URL here is used as the advertiser ID) or <code>googleAdvertiserId</code> (AR... or a Transparency Center advertiser URL), <code>regions</code> (ISO country codes such as US, GB, JP, or "anywhere"; a list or a comma-separated text; default US). A competitor listed twice (e.g. for two clients) is checked and billed once, and read in the union of the countries of both entries (at most 20 countries per competitor).

## `slackWebhookUrl` (type: `string`):

Optional. One digest per run, grouped by client and competitor. Create one in Slack: Apps > Incoming Webhooks (https://hooks.slack.com/services/...). Slack workflow and trigger URLs, and other URLs that cannot receive the digest as written, are refused before anything is read or charged (the run ends as aborted, Invalid input). Stored as a secret; not written to logs or outputs. If a delivery fails, the run still ends as succeeded after the checks and charges (the new ads are in the dataset; the status message starts with the failed notification), and a summary of it (new-ad counts, signals and the top 5 ads per competitor) is sent to this same URL with the next run (a digest with no new ads or signals has nothing to send again). If you change this URL, what was still undelivered for the old one goes to the new one for the competitors still watched under the same label and client (the rest is said in the status message); a summary too long for one Slack message is spread over the following messages. Temporary failures (timeouts, HTTP 408, 429, 5xx) do not stop the run; if Slack refuses the digest 3 runs in a row with a permanent answer (such as 404 or 410), that run ends as aborted and the status message asks to check this URL (one successful delivery resets the count). One Slack message holds at most 50 blocks and about 40,000 characters; competitors beyond that are counted as +N more in the dataset.

## `webhookUrl` (type: `string`):

Optional. Receives the same digest as JSON (POST, https only, public addresses only, redirects are not followed) - for n8n, Make, Zapier or your own endpoint. Other URLs are refused before anything is read or charged (the run ends as aborted, Invalid input). Each delivery carries an idempotencyKey (also as the Idempotency-Key header). The body is kept under 1 MB: above that each competitor lists its top 5 new ads (with the full count) and what still does not fit is counted in truncated (every row is in the dataset). Stored as a secret. If a delivery fails, the run still ends as succeeded after the checks and charges (the new ads are in the dataset; the status message starts with the failed notification), and a summary of it is sent to this same URL with the next run (a digest with no new ads or signals has nothing to send again). If you change this URL, what was still undelivered for the old one goes to the new one for the competitors still watched under the same label and client (the rest is said in the status message). Temporary failures (timeouts, HTTP 408, 429, 5xx) do not stop the run; if the URL refuses the digest 3 runs in a row with a permanent answer (HTTP 3xx/4xx other than 408/429, or an internal address at connect time), that run ends as aborted and the status message asks to check this URL (one successful delivery resets the count).

## `language` (type: `string`):

Language of the Slack digest. English digests show dates in UTC, Japanese digests in Japan time.

## `sendQuietDigest` (type: `boolean`):

Off: quiet runs (no new ads, no signals, no failures) send nothing. On: every run sends a short "no new ads" status - useful on a weekly schedule.

## `maxNewAdsPerAdvertiser` (type: `integer`):

New ads beyond this are counted in the digest but not listed or billed, and are not reported again.

## `baselineDays` (type: `integer`):

The first check of a competitor (or country) summarises ads shown in this many days, free. Ads first shown before this window (minus the overlap), or before the window of the country's first check, are not billed as new. A competitor or country not checked for clearly longer than this window (the window plus the larger of the overlap and a quarter of the window) gets a fresh free baseline instead of a bill for everything that appeared meanwhile.

## `includeBaselineRows` (type: `boolean`):

Off: the first check returns a free summary (counts, formats, latest 10) and the 5 latest baseline ads as free baseline_sample rows. On: every baseline ad is written as a row, billed per row (baseline-row).

## `stateKey` (type: `string`):

Optional name of the memory of already-seen ads. Leave empty in a task: each task gets its own memory automatically (named after the task). Outside a task (API calls, a schedule that starts the Actor itself), an empty state key uses the shared memory `main`. The memory is kept per competitor, so editing the watchlist keeps it: an added competitor starts with a free baseline, a removed one is simply no longer checked. To run a different watchlist outside a task, give it its own state key (two watchlists on one key share one memory; the run log warns when one key is used with different Slack/webhook destinations). "default" counts as empty. Two runs with the same key at the same time: the second waits up to 5 minutes, then stops without checking or charging (run ends as aborted).

## `launchThreshold` (type: `integer`):

Flag a "campaign launch" when a competitor starts at least this many new ads within 7 days.

## `maxChargeUsd` (type: `number`):

The run stops between competitors before its charges would exceed this amount; the rest are listed as "not checked", go first next run, and unpaid new ads are kept for the next run. A competitor that alone does not fit in what is left (a very large inventory) is listed as "not checked" and the run goes on with the others. A competitor that was read but could not be paid for goes after the others next run (and is not read again for up to 7 days while what is left stays below its last read; that estimate is at most what the current read limit allows and is dropped when its countries change). The run reports the amount that would cover every competitor. The lower of this and the platform's "Maximum cost per run" applies. 0 checks nothing.

## `minHoursBetweenChecks` (type: `integer`):

A competitor whose last check was in a run that started less than about this many hours ago (10% slack for schedule jitter) is skipped (not read, not billed). Keeps a too-frequent schedule from re-reading the Transparency Center. 0 turns it off.

## `maxRunMinutes` (type: `integer`):

No new competitor is started after this many minutes (at most 120; also 2 minutes before the platform timeout); the rest are listed as "not checked" and go first next run. The digest is still sent.

## `maxAdsReadPerAdvertiser` (type: `integer`):

Safety limit on how deep one competitor's ad list is read per run. Reads beyond 200 ads (the ads read in each country, added up over its countries) are billed per 40 (google-large-inventory-page). If the limit is reached before every country of a competitor was read, the check is billed for what was read and the unread countries are read first next run.

## `overlapHours` (type: `integer`):

Re-reads this much before the previous check so ads that appear late in the Transparency Center are not missed. Already-seen ads are not reported or billed twice. A longer overlap reads a little deeper each run (for very large advertisers this can add large-inventory pages).

## `proxyConfiguration` (type: `object`):

Residential Apify Proxy (the default) is required in practice: Google Ads Transparency Center answers datacenter proxy IPs with HTTP 429 on every request, so Apify Proxy without a group is switched to Residential. Requests are sent one at a time; after a rate limit, server or network error the client slows down and retries a request at most twice. A country that still fails ends that competitor's check (its other countries are not read; nothing is billed, it is retried next run), and 3 such competitors in a row stop the run. A competitor that failed goes after the others next run. A competitor is read at most once per minHoursBetweenChecks.

## `spreadsheetSafeText` (type: `boolean`):

Turn on if you open the dataset in Excel, Google Sheets or another spreadsheet (CSV/XLSX export). Text that starts with =, +, -, @ (or a tab or line break) gets a leading apostrophe, so the spreadsheet shows it as text instead of running it as a formula. Advertiser names are written by the advertiser. Off by default because it changes those values for JSON consumers.

## Actor input object example

```json
{
  "watchlist": [
    {
      "label": "Example",
      "googleDomain": "patagonia.com",
      "regions": [
        "US"
      ]
    }
  ],
  "language": "en",
  "sendQuietDigest": false,
  "maxNewAdsPerAdvertiser": 50,
  "baselineDays": 30,
  "includeBaselineRows": false,
  "launchThreshold": 5,
  "maxChargeUsd": 1,
  "minHoursBetweenChecks": 1,
  "maxRunMinutes": 30,
  "maxAdsReadPerAdvertiser": 1000,
  "overlapHours": 48,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  },
  "spreadsheetSafeText": false
}
```

# Actor output Schema

## `results` (type: `string`):

One summary row per competitor checked, a row per new ad, and on a first check the free baseline sample rows.

## `runSummary` (type: `string`):

What the run did: competitors checked or not, new ads, charges, notifications and warnings.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "watchlist": [
        {
            "label": "Example",
            "googleDomain": "patagonia.com",
            "regions": [
                "US"
            ]
        }
    ],
    "proxyConfiguration": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ]
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("domestic_buffalograss/competitor-ad-alerts").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "watchlist": [{
            "label": "Example",
            "googleDomain": "patagonia.com",
            "regions": ["US"],
        }],
    "proxyConfiguration": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
    },
}

# Run the Actor and wait for it to finish
run = client.actor("domestic_buffalograss/competitor-ad-alerts").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "watchlist": [
    {
      "label": "Example",
      "googleDomain": "patagonia.com",
      "regions": [
        "US"
      ]
    }
  ],
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}' |
apify call domestic_buffalograss/competitor-ad-alerts --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,domestic_buffalograss/competitor-ad-alerts"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/dptpY1cEOj0rVOdif/builds/MCult6MMJraPyl2fD/openapi.json
