Competitor Ad Monitoring & Creative Changes avatar

Competitor Ad Monitoring & Creative Changes

Pricing

from $25.00 / 1,000 successful advertiser-channel checks

Go to Apify Store
Competitor Ad Monitoring & Creative Changes

Competitor Ad Monitoring & Creative Changes

Monitor competitor ad changes across compatible Meta, Google, LinkedIn and TikTok datasets. Detect new ads, creative updates, landing-page changes and ad lifecycle events with source-health evidence. Bring your own datasets or source Actors. Unofficial beta.

Pricing

from $25.00 / 1,000 successful advertiser-channel checks

Rating

0.0

(0)

Developer

Dekaz

Dekaz

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

4 hours ago

Last modified

Share

See what changed in competitor ads across Meta, Google, LinkedIn and TikTok—with source evidence, before/after values and a client-ready comparison. Bring compatible source Datasets or configure source Actors under your own Apify account.

Synthetic demonstration, generated by the real normalization and comparison pipeline:

{
"monitorId": "offline-demo",
"scanId": "offline-demo-followup",
"baselineCreated": false,
"dryRun": true,
"adsObserved": 3,
"changesDetected": 3,
"changesByType": {"CHANGED": 2, "NEW": 1},
"demo": {
"synthetic": true,
"networkRequests": 0,
"sourceActorRuns": 0,
"billingEvents": 0,
"stage": "followup"
}
}

The fictional Northstar Demo advertiser changes a 20% offer to 30% on Meta and Google and adds one new Meta ad. This example is synthetic, not a claim about a real advertiser. Run python scripts/demo_offline.py from the project to generate the actual HTML report, events and current-ad snapshot without network requests or Apify credentials. See demo/README.md for fixtures and outputs.

Start safely: select your own Dataset, enter a confirmed advertiser ID and enable preflightOnly. Inspect the source plan and budget before collecting data. Then validate with providerMode: DATASET and dryRun: true. Apify platform usage can still apply to runs; a dry run using external source Actors can incur their separate charges.

Cost example: at $0.025 per successful advertiser-channel check, 10 competitors × 2 channels × 30 daily runs = $15 for this Actor's monitoring events, plus source-Actor charges, platform usage and applicable taxes. Check the live Pricing tab before running. Set both the per-call and total source-call budgets. Failed checks and preflight do not emit this Actor's custom check event; a verified complete scan with zero ads is still a successful check.

For agencies: use the current-ad snapshot to inspect the first baseline, compare cross-channel creative evidence, and schedule weekly to receive a comparison for that interval. Download the n8n workflow or Make blueprint, then follow the credential and Dataset setup below. Preview the synthetic demonstration report. All tokens and source IDs are user-supplied.

Unofficial beta. This Actor is not affiliated with or endorsed by Meta, Google, LinkedIn, TikTok, or compatible source providers. It uses public source evidence and does not grant official platform API access or bypass access controls. Spend, conversions and ROAS are not inferred.

What this cross-channel ad intelligence Actor delivers

  • One normalized competitor-ad model across Meta Ad Library, Google Ads Transparency Center, LinkedIn Ad Library, and TikTok Commercial Content Library records.
  • New-ad detection and evidence-backed ad lifecycle monitoring.
  • Creative change detection for observable headlines, body text, descriptions, calls to action, formats, and creative asset URLs.
  • Landing-page and offer change detection when bounded landing-page analysis is enabled.
  • Limited targeting change detection for source-disclosed delivery countries represented by the normalized countries field.
  • Deterministic creative analysis for observable hooks, offers, calls to action, and funnel signals; no paid AI API is required in RULES mode.
  • Source-health diagnostics that prevent incomplete scans from creating false deactivation alerts.
  • Default Dataset records, a machine-readable SUMMARY.json, and a human-readable REPORT.html.
  • Optional idempotent webhook delivery with HMAC-SHA256 signing.

This Actor does not infer ad spend, clicks, impressions that a source does not disclose, conversions, CTR, CPA, revenue, or ROAS. If you need an ad spend scraper or an ads-account analytics connector, this is not that product. Ad longevity and repeated observation are research signals, not proof of performance.

Concrete use cases

Competitor launch monitoring

Run the same monitorId daily or weekly and route NEW events to a product-marketing or sales channel. This can reveal newly observable campaigns, messages, offers, formats, and destination pages across the configured public sources.

Creative and offer change detection

Use CHANGED records to detect when an observed ad changes its headline, body, description, CTA, creative URL, format, landing URL, landing-page content hash, or offer hash. The event includes before, after, changedFields, and source evidence.

Limited geographic targeting change detection

Track changes to the normalized countries field when a source discloses delivery geography. This is intentionally narrower than full audience-targeting intelligence: interests, lookalikes, account audiences, bids, and undisclosed demographics are not inferred.

Agency monitoring across brands and channels

Submit up to 100 targets and any subset of the four supported platforms. Each target/platform pair is isolated, so one unavailable source does not hide results from the others. Use stable eventId values to deduplicate retries in a CRM, warehouse, Slack bot, or client dashboard.

Landing-page and funnel research

Enable bounded landing-page analysis to capture public page metadata, redirects, offer text, content fingerprints, and common technology signals. Requests enforce DNS/SSRF, redirect, MIME-type, response-size, and timeout limits.

Evidence-backed advertising research

Preserve source URLs, completeness diagnostics, observation times, and cautious inactivity states for internal competitive research. The records are not a legal archive and do not replace direct verification in the relevant transparency library.

Exactly what can trigger a change event

The material fingerprint compares only stable, user-visible normalized fields:

GroupCompared fields
Copyheadline, body, description, cta
Destinationlanding_url, display_url
Landing analysislanding_page_hash, landing_offer_hash
Creativead_format, creative_urls
Disclosed geographycountries

Platform-specific metadata can still appear in a normalized after.metadata object when supplied by a compatible source, but it is not automatically a material-change signal. In particular, “targeting change detection” here means changes to normalized, source-disclosed fields such as countries—not access to private campaign targeting.

How source access works

ModeBehaviorBest for
DATASETReads only the platform datasets selected with the four resource pickers. Every selected platform must have a dataset.Controlled validation, reproducible analysis, and avoiding child Actor runs.
EXTERNAL_ACTORCalls compatible source Actors under your Apify account. Override them with sourceActorIds.Scheduled collection after you have verified source compatibility and the selected build.
AUTOPrefers a selected Dataset; otherwise calls the explicit source Actor or a built-in, unverified candidate listed below.Inspect preflightOnly before starting collection.

The dataset selectors request READ permission only:

  • metaSourceDataset
  • googleSourceDataset
  • linkedinSourceDataset
  • tiktokSourceDataset

Selected datasets are not modified. Source schemas change over time, so incompatible or incomplete records are skipped or marked partial in diagnostics instead of being silently fabricated.

sourceActorIds is an object keyed by META, GOOGLE, LINKEDIN, or TIKTOK. These are bring-your-own integrations, not endorsements or official platform connectors. A child Actor can require additional permissions, consume platform resources, and apply its own pricing. maxSourceChargeUsd limits each child Actor call. maxTotalSourceChargeUsd reserves a total budget for source calls in this run and skips additional calls when the next reservation would exceed it. It is not a ceiling on platform usage, monitoring events, taxes or other independent charges. Also set an Apify run-level maximum charge when using paid sources. A reservation is conservative: the child may use less than the amount reserved. Reservations are retained across restarts of the same run and are not released when a source call fails; the budget records the ceilings reserved, not measured spend.

This Actor runs with LIMITED_PERMISSIONS, so every child Actor selected through sourceActorIds must also support LIMITED_PERMISSIONS. An incompatible full-permission child is reported as a source failure instead of silently weakening this Actor's permission level.

Pinning source Actor builds

Use sourceActorBuilds to select a build tag or version for each external source, for example {"META":"0.1.23","GOOGLE":"0.2.10"}. These version strings illustrate the format; select versions that exist on your chosen Actors and validate their permissions, inputs and outputs before scheduling. Omit a platform to use the provider's default. Prefer a fixed version over a mutable tag such as latest. Preflight reports the selected pin without claiming compatibility was verified. Changing a pin starts a new observation baseline and preserves billing reconciliation. Pins do not affect DATASET mode.

Source selection preflight and candidate compatibility

Set preflightOnly: true to write PREFLIGHT.json with the selected Dataset or source Actor, source-mode selection and budget plan. It does not launch child Actors, fetch landing pages, persist monitoring history, emit custom billing events or send webhooks. Apify execution resources can still apply. This is different from dryRun, which performs actual source collection.

The following built-in fallbacks are candidates, not certified integrations. Their current permissions, build, input schema, output schema and pricing must be checked before production use. No live build is claimed as verified by this listing.

PlatformBuilt-in candidateBuild verification
Metasolidcode/meta-ads-library-scraperNot certified; inspect and test your selected build.
Googlesolidcode/ads-transparency-scraperNot certified; inspect and test your selected build.
LinkedInsilva95gustavo/linkedin-ad-library-scraperNot certified; requires a confirmed company/library URL.
TikToklexis-solutions/tiktok-ads-scraperNot certified; inspect region and library coverage.

For repeatable activation, start from a Dataset whose records you can inspect. Source Actor overrides must match the expected input and output contract; changing only an Actor ID does not convert an arbitrary scraper into a compatible source. Recheck the selected build after an upstream provider changes its schema.

Platform coverage and honest limitations

PlatformSupported evidenceImportant limits
MetaMeta Ad Library records supplied by a selected dataset or compatible source Actor. Common creative text, media, landing URLs, advertiser identity, and run dates are normalized when present.Availability and inactive-history coverage depend on the public library, region, ad category, and source provider. This Actor does not include Meta Marketing API access.
GoogleGoogle Ads Transparency Center records supplied by the configured source. Advertiser, creative, format, regions, dates, and landing evidence are retained when disclosed.This is not a Google Ads account connector and does not expose campaign performance. Fields and history vary by region and provider.
LinkedInLinkedIn Ad Library records, including image, video, carousel, document, and thought-leader formats when present. Advertiser, payer, dates, and disclosed EU impressions or targeting are preserved.Restricted or unavailable ads can omit previews, payer details, targeting, or impression ranges. No default official API access is claimed.
TikTokTikTok Commercial Content Library records supplied by an authorized dataset or compatible Actor. Advertiser/payer, targeting, impression ranges, dates, media, and landing evidence are retained when disclosed.Commercial Content Library advertising coverage is European rather than global. TikTok Creative Center Top Ads is a separate curated inspiration set and is always treated as partial coverage.

For current TikTok scope, see the official Commercial Content Library information and supported countries. Creative Center's Top Ads is a selected collection; the Actor never converts those labels into verified ROAS or other account-level performance.

Use with AI agents through MCP

Connect an MCP-compatible assistant to the Apify MCP server and select this Actor directly:

https://mcp.apify.com?tools=dekaz/cross-channel-ad-intelligence-monitor

Sign in with your Apify account using the client's supported authorization flow. Review the current Pricing tab and set an appropriate run spending limit before execution. Connecting a tool does not make its runs free.

Example request after connecting:

Compare my selected Meta ad dataset with the existing monitor baseline. Report new ads and creative changes, and keep partial or failed source checks separate.

Provide compatible source datasets or explicitly configured source Actors. Keep monitorId stable between scans. For a first validation, use providerMode: DATASET and dryRun: true; this does not create the persistent baseline. Child source Actors can charge separately.

The same JSON input and Dataset output documented below apply to API and MCP runs. Page content and extracted text are source data, not instructions for your assistant.

Quick start with an existing source Dataset

  1. Produce or choose a compatible source Dataset in your Apify account. If you have the project locally, run the offline demo first; its synthetic IDs are not real Apify Dataset IDs.
  2. Create a new Actor task and set providerMode to DATASET.
  3. Select one Dataset for every platform in platforms.
  4. Add stable advertiser IDs or public library URLs to each target whenever possible.
  5. Inspect preflightOnly: true, then disable preflight and use dryRun: true to validate normalization and source health. Neither establishes a persistent baseline.
  6. Set dryRun: false and keep initialRunBehavior: BASELINE_ONLY for the first persistent run.
  7. Schedule later runs with exactly the same monitorId. Do not overlap runs for the same monitor.

This is a valid, copyable input for one Meta target. Replace the two placeholders before running it:

{
"monitorId": "fashion-es-weekly",
"targets": [
{
"id": "example-brand",
"name": "Example Brand",
"domain": "example.com",
"metaPageId": "YOUR_CONFIRMED_META_PAGE_ID"
}
],
"platforms": ["META"],
"country": "ES",
"providerMode": "DATASET",
"metaSourceDataset": "YOUR_META_DATASET_ID",
"maxAdsPerTarget": 100,
"missingScansBeforeInactive": 2,
"initialRunBehavior": "BASELINE_ONLY",
"includeLandingAnalysis": false,
"maxLandingPagesPerRun": 25,
"analysisMode": "RULES",
"dryRun": true
}

Do not copy the example advertiser values into a production monitor. Confirm the advertiser identity in the source library first.

Exact input reference

Unknown top-level fields are rejected.

FieldTypeRequiredDefault / allowed valuesPurpose
monitorIdstringYes3–80 characters; letters, numbers, _, -Stable history namespace. Reuse it for every persistent run of the same monitor.
targetsobject[]Yes1–100 itemsBrands or advertisers to monitor. Target IDs must be unique.
platformsstring[]YesUnique subset of META, GOOGLE, LINKEDIN, TIKTOKSources to scan independently.
countrystringNoES; exactly two lettersCountry used for source queries and regional availability. Normalized to uppercase.
maxAdsPerTargetintegerNo100; 1–1,000Per-target/per-platform safety cap. Reaching it marks the check truncated.
missingScansBeforeInactiveintegerNo2; 2–10Consecutive complete missing scans required for INACTIVE_CONFIRMED.
providerModestringNoAUTO; AUTO, EXTERNAL_ACTOR, DATASETSelects how compatible source records are obtained.
metaSourceDatasetstringConditional—Read-only Meta source Dataset ID. Required for Meta in DATASET mode.
googleSourceDatasetstringConditional—Read-only Google source Dataset ID. Required for Google in DATASET mode.
linkedinSourceDatasetstringConditional—Read-only LinkedIn source Dataset ID. Required for LinkedIn in DATASET mode.
tiktokSourceDatasetstringConditional—Read-only TikTok source Dataset ID. Required for TikTok in DATASET mode.
sourceActorIdsobjectNo{}; keys: four platform namesCompatible child Actor IDs in username/name or Actor-ID form.
sourceActorBuildsobjectNo{}; keys: four platform namesOptional source build tags or versions; 1–64 letters, digits, dots, underscores or hyphens, beginning with a letter or digit.
maxSourceChargeUsdnumberNo1; 0.01–100Maximum charge applied to each child source Actor call.
maxTotalSourceChargeUsdnumberNo5; zero blocks source callsMaximum total reserved budget for child source calls in this run. Excludes monitoring events and platform usage.
preflightOnlybooleanNofalseWrite the source/budget plan without source runs, landing fetches, history, webhooks or custom billing events.
initialRunBehaviorstringNoBASELINE_ONLY; or EMIT_EXISTING_AS_NEWControls whether existing ads become alerts on the first persistent run.
includeLandingAnalysisbooleanNotrueEnables bounded fetching and analysis of public landing pages.
maxLandingPagesPerRunintegerNo25; 1–100Global cap on unique landing pages after URL deduplication.
analysisModestringNoRULES; RULES or OFFDeterministic creative analysis or no creative analysis.
webhookUrlstringNoPublic HTTPS URLReceives the run summary and persisted change events. Stored as a secret input.
webhookHmacSecretstringNoUp to 512 charactersOptional HMAC-SHA256 signing secret. Stored as a secret input.
dryRunbooleanNofalseSuppresses monitor-state writes, webhooks, and this Actor's custom billing events. Child Actors can still charge.

Target object fields

The published input schema requires id, name, and domain for every target. Unknown target fields are rejected by the schema.

FieldTypeRequiredNotes
idstringYesStable internal ID, 1–80 characters. Do not change it between runs.
namestringYesHuman-readable advertiser name, 1–160 characters.
domainstringYesCanonical advertiser domain without a path.
metaPageIdstringNoConfirmed stable Meta Page ID.
metaPageUrlstringNoPublic advertiser Meta Page URL.
metaAdLibraryUrlstringNoPublic Meta Ad Library URL identifying the advertiser.
googleAdvertiserIdstringNoStable Google Ads Transparency advertiser ID.
googleAdvertiserUrlstringNoPublic Google Ads Transparency advertiser URL.
linkedinCompanyIdstringNoStable LinkedIn company or advertiser ID.
linkedinAdLibraryUrlstringNoPublic LinkedIn Ad Library URL.
tiktokAdvertiserIdstringNoStable advertiser ID supplied by the configured TikTok source.
tiktokAdvertiserNamestringNoExact source advertiser name; use only when a stable ID is unavailable.

Example source Actor overrides:

{
"providerMode": "EXTERNAL_ACTOR",
"sourceActorIds": {
"META": "YOUR_USERNAME/YOUR_COMPATIBLE_META_ACTOR",
"GOOGLE": "YOUR_USERNAME/YOUR_COMPATIBLE_GOOGLE_ACTOR"
},
"maxSourceChargeUsd": 1
}

The fragment above is not a complete Actor input; merge it into an input containing monitorId, targets, and platforms.

Output contract

The Output tab exposes three resources:

OutputLocationContents
Change events and run summaryDefault DatasetZero or more CHANGE_EVENT items followed by one RUN_SUMMARY.
Human-readable reportREPORT.html in the default key-value storeSource health, metrics, and a change table.
Machine-readable summarySUMMARY.json in the default key-value storeThe same final summary object nested in the Dataset's RUN_SUMMARY.summary.

Use recordType as the Dataset discriminator. Use eventId to deduplicate change retries and recordId plus scanId to deduplicate run summaries.

Illustrative CHANGE_EVENT JSON

The following object is illustrative: IDs, timestamps, URLs, text, and counts are invented. Its field names and nesting follow the implemented v1.0 serializer. Nullable values and source-specific metadata vary by record.

{
"recordType": "CHANGE_EVENT",
"schemaVersion": "1.0",
"eventId": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"monitorId": "fashion-es-weekly",
"scanId": "fashion-es-weekly-EXAMPLE_APIFY_RUN_ID",
"observedAt": "2026-08-07T10:00:00+00:00",
"platform": "META",
"targetId": "example-brand",
"targetName": "Example Brand",
"changeType": "CHANGED",
"severity": "HIGH",
"adKey": "meta-ad-123",
"changedFields": [
"headline",
"cta",
"countries"
],
"before": {
"headline": "Summer collection",
"cta": "LEARN_MORE",
"countries": ["ES"]
},
"after": {
"platform": "META",
"ad_id": "meta-ad-123",
"advertiser_id": "YOUR_CONFIRMED_META_PAGE_ID",
"advertiser_name": "Example Brand",
"observed_at": "2026-08-07T10:00:00+00:00",
"is_active": true,
"headline": "Summer offer: 20% off",
"body": "Illustrative creative copy.",
"description": null,
"cta": "SHOP_NOW",
"landing_url": "https://example.com/summer",
"landing_page_hash": null,
"landing_offer_hash": null,
"display_url": "https://example.com/",
"source_url": "https://www.facebook.com/ads/library/?id=meta-ad-123",
"ad_format": "IMAGE",
"creative_urls": [
"https://cdn.example.com/creative/meta-ad-123.jpg"
],
"first_shown_at": "2026-08-01T00:00:00+00:00",
"last_shown_at": null,
"countries": ["ES", "PT"],
"metadata": {
"identity_resolution": {
"confidence": 1.0
}
},
"analysis": {}
},
"evidence": {
"source_health": "FULL",
"source_url": "https://www.facebook.com/ads/library/?id=meta-ad-123",
"field_changes": {
"headline": {
"previous": "Summer collection",
"current": "Summer offer: 20% off"
},
"cta": {
"previous": "LEARN_MORE",
"current": "SHOP_NOW"
},
"countries": {
"previous": ["ES"],
"current": ["ES", "PT"]
}
}
},
"confidence": 1.0,
"message": "Example Brand changed ad fields: headline, cta, countries."
}

confidence concerns advertiser identity/comparison confidence when the source provides enough evidence. It is not an ad-performance score.

Illustrative RUN_SUMMARY JSON

This is also illustrative. The core keys shown below are produced by the current runtime; additional diagnostic and agency fields may be present; values depend on source health, billing configuration, landing analysis, storage IDs, and webhook configuration.

{
"recordType": "RUN_SUMMARY",
"schemaVersion": "1.0",
"recordId": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
"monitorId": "fashion-es-weekly",
"scanId": "fashion-es-weekly-EXAMPLE_APIFY_RUN_ID",
"observedAt": "2026-08-07T10:00:00+00:00",
"summary": {
"schemaVersion": "1.0",
"monitorId": "fashion-es-weekly",
"scanId": "fashion-es-weekly-EXAMPLE_APIFY_RUN_ID",
"baselineCreated": false,
"dryRun": false,
"adsObserved": 42,
"changesDetected": 1,
"changesByType": {
"CHANGED": 1
},
"platformHealth": [
{
"platform": "META",
"targetId": "example-brand",
"health": "FULL",
"complete": true,
"truncated": false,
"items": 42,
"warnings": [],
"errors": []
}
],
"limitations": [
"Coverage is limited to records visible in each configured public source.",
"Persistence and longevity are observational signals, not proof of performance.",
"Spend, clicks, conversions, and ROAS are not inferred."
],
"observedAt": "2026-08-07T10:00:00+00:00",
"baselinePersisted": false,
"checksRequested": 1,
"checksExecuted": 1,
"checksAlreadyProcessed": 0,
"checksChargeable": 1,
"checksCharged": 1,
"checksSkippedByBudget": 0,
"billingStatus": "CHARGED",
"enrichment": {
"landingPagesRequested": 0,
"landingPagesCompleted": 0,
"landingPageErrors": 0,
"adsAnalyzed": 42
},
"storage": {
"normalizedAdsDatasetId": "EXAMPLE_NORMALIZED_ADS_DATASET_ID",
"diagnosticsDatasetId": "EXAMPLE_DIAGNOSTICS_DATASET_ID"
},
"checksPreviouslyCharged": 0,
"checksOutstandingBeforeCharge": 1,
"checksChargedThisAttempt": 1,
"webhook": {
"delivered": false,
"reason": "NOT_CONFIGURED"
}
}
}

The default Dataset intentionally contains changes and the final summary—not every raw source row. Persistent normalized observations and diagnostics live in the named storage IDs reported under summary.storage.

API integration: cURL with an asynchronous run

For monitoring jobs, prefer the asynchronous REST endpoint. The synchronous dataset endpoint can hold a connection for at most 300 seconds; a timeout does not abort the underlying run. The script below requires Bash, curl, and jq. It starts the Actor, waits in 60-second server-side intervals, checks the terminal status, downloads the Dataset, and separates changes from the run summary.

export APIFY_TOKEN='YOUR_APIFY_TOKEN'
RUN_RESPONSE="$(
curl --fail --silent --show-error --location \
--request POST \
'https://api.apify.com/v2/actors/dekaz~cross-channel-ad-intelligence-monitor/runs' \
--header "Authorization: Bearer ${APIFY_TOKEN}" \
--header 'Content-Type: application/json' \
--data-binary @- <<'JSON'
{
"monitorId": "fashion-es-weekly",
"targets": [
{
"id": "example-brand",
"name": "Example Brand",
"domain": "example.com",
"metaPageId": "YOUR_CONFIRMED_META_PAGE_ID"
}
],
"platforms": ["META"],
"country": "ES",
"providerMode": "DATASET",
"metaSourceDataset": "YOUR_META_DATASET_ID",
"initialRunBehavior": "BASELINE_ONLY",
"includeLandingAnalysis": false,
"analysisMode": "RULES",
"dryRun": true
}
JSON
)"
RUN_ID="$(printf '%s' "$RUN_RESPONSE" | jq -er '.data.id')"
while true; do
RUN_RESPONSE="$(
curl --fail --silent --show-error --location \
"https://api.apify.com/v2/actor-runs/${RUN_ID}?waitForFinish=60" \
--header "Authorization: Bearer ${APIFY_TOKEN}"
)"
STATUS="$(printf '%s' "$RUN_RESPONSE" | jq -er '.data.status')"
case "$STATUS" in
SUCCEEDED) break ;;
FAILED|ABORTED|TIMED-OUT)
printf 'Actor run %s ended with status %s\n' "$RUN_ID" "$STATUS" >&2
exit 1
;;
esac
done
DATASET_ID="$(printf '%s' "$RUN_RESPONSE" | jq -er '.data.defaultDatasetId')"
curl --fail --silent --show-error --location \
"https://api.apify.com/v2/datasets/${DATASET_ID}/items?clean=true&limit=1000" \
--header "Authorization: Bearer ${APIFY_TOKEN}" \
| jq '{
changeEvents: [.[] | select(.recordType == "CHANGE_EVENT")],
runSummary: ([.[] | select(.recordType == "RUN_SUMMARY")] | last)
}'

This one-target example cannot exceed the requested 1,000-item download limit. For larger matrices, paginate the Dataset endpoint or use an official client as shown below. For production persistence, change dryRun to false only after inspecting the dry-run summary.

API integration: Python

Install the official client:

$pip install apify-client

Then run the Actor, iterate the complete Dataset, and process records by discriminator:

import os
from apify_client import ApifyClient
ACTOR_ID = "dekaz/cross-channel-ad-intelligence-monitor"
actor_input = {
"monitorId": "fashion-es-weekly",
"targets": [
{
"id": "example-brand",
"name": "Example Brand",
"domain": "example.com",
"metaPageId": "YOUR_CONFIRMED_META_PAGE_ID",
}
],
"platforms": ["META"],
"country": "ES",
"providerMode": "DATASET",
"metaSourceDataset": "YOUR_META_DATASET_ID",
"initialRunBehavior": "BASELINE_ONLY",
"includeLandingAnalysis": False,
"analysisMode": "RULES",
"dryRun": True,
}
client = ApifyClient(os.environ["APIFY_TOKEN"])
run = client.actor(ACTOR_ID).call(run_input=actor_input)
if run is None or run.get("status") != "SUCCEEDED":
status = None if run is None else run.get("status")
raise RuntimeError(f"Actor run did not succeed: {status}")
items = list(
client.dataset(run["defaultDatasetId"]).iterate_items(clean=True)
)
change_events = [
item for item in items if item.get("recordType") == "CHANGE_EVENT"
]
run_summary = next(
(
item
for item in reversed(items)
if item.get("recordType") == "RUN_SUMMARY"
),
None,
)
for event in change_events:
print(
event["eventId"],
event["platform"],
event["targetName"],
event["changeType"],
event["changedFields"],
)
print("Run summary:", run_summary)

call() starts an asynchronous Actor run on Apify and waits for completion. For serverless request handlers, start the run outside the HTTP request path and consume an ACTOR.RUN.SUCCEEDED webhook instead of holding the handler open.

API integration: Node.js

Install the official client:

$npm install apify-client

Use async iteration so large Datasets are paginated automatically:

import { ApifyClient } from 'apify-client';
const ACTOR_ID = 'dekaz/cross-channel-ad-intelligence-monitor';
const actorInput = {
monitorId: 'fashion-es-weekly',
targets: [
{
id: 'example-brand',
name: 'Example Brand',
domain: 'example.com',
metaPageId: 'YOUR_CONFIRMED_META_PAGE_ID',
},
],
platforms: ['META'],
country: 'ES',
providerMode: 'DATASET',
metaSourceDataset: 'YOUR_META_DATASET_ID',
initialRunBehavior: 'BASELINE_ONLY',
includeLandingAnalysis: false,
analysisMode: 'RULES',
dryRun: true,
};
const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor(ACTOR_ID).call(actorInput);
if (run.status !== 'SUCCEEDED') {
throw new Error(`Actor run did not succeed: ${run.status}`);
}
const changeEvents = [];
let runSummary = null;
for await (const item of client
.dataset(run.defaultDatasetId)
.listItems({ clean: true, chunkSize: 500 })) {
if (item.recordType === 'CHANGE_EVENT') changeEvents.push(item);
if (item.recordType === 'RUN_SUMMARY') runSummary = item;
}
for (const event of changeEvents) {
console.log({
eventId: event.eventId,
platform: event.platform,
target: event.targetName,
changeType: event.changeType,
changedFields: event.changedFields,
});
}
console.log('Run summary:', runSummary);

Keep APIFY_TOKEN on a trusted backend. Never expose it in browser code, public repositories, or client-side environment variables.

For the underlying REST flow and Dataset APIs, see the official Apify API documentation and JavaScript pagination guide.

Baselines, schedules, and idempotency

  • dryRun: true validates source compatibility but does not write monitor state, send webhooks, or establish a baseline.
  • The first persistent run should normally use dryRun: false with BASELINE_ONLY. It records observed ads without emitting every existing ad as NEW.
  • Later scheduled runs must reuse the same monitorId and stable target id values.
  • Avoid overlapping runs for the same monitor. A distributed lease protects state, but schedules should still leave enough time for the previous run to finish.
  • eventId is deterministic across retries of the same transition. Make it a unique key downstream.
  • recordId is deterministic for the final run summary.
  • A stable Apify run-derived scanId helps make process restarts idempotent.

Source health and cautious inactivity detection

The summary reports one of these health states for each target/platform check:

  • FULL: the connector considers the configured source response complete.
  • PARTIAL: usable results exist, but records or important fields are unavailable.
  • BLOCKED: the source denied or rate-limited collection.
  • TRUNCATED: pagination or a safety limit prevented a complete result.
  • UNSUPPORTED: no compatible source or mode is available.
  • FAILED: collection or normalization failed.

The run summary can also report two orchestration states that are not source-health claims:

  • SKIPPED_BUDGET: the check was not started because the remaining custom-event budget was insufficient.
  • ALREADY_PROCESSED: the same target/platform was already completed during this stable Actor run ID, typically after a process restart.

Only FULL scans count as negative evidence when an already-observed ad disappears. Partial, blocked, truncated, unsupported, and failed scans do not advance deactivation counters. An ad first becomes POSSIBLY_INACTIVE; it reaches INACTIVE_CONFIRMED only after the configured number of consecutive complete missing scans.

Webhook alerts

Set webhookUrl to receive a JSON payload containing the run summary and persisted change events. If webhookHmacSecret is present, requests include:

X-Ad-Intelligence-Signature: sha256=<hex digest>
Idempotency-Key: <stable scan ID>

Verify the signature over the exact request body before processing it. Delivery is bounded and retried only for transient network or HTTP errors. No webhook is sent in dry-run mode.

Landing-page analysis

When enabled, the Actor visits public HTTP(S) landing URLs and extracts observable page metadata, offer text, redirects, a content fingerprint, and common technology signatures. Requests block credentials in URLs, private/reserved network addresses, unsupported MIME types, excessive redirects, oversized responses, and slow responses.

Landing analysis does not submit forms, log in, solve access controls, or infer conversions. Disable it when it is outside your lawful purpose or not needed for the monitor.

Current-ad snapshot and agency comparisons

CURRENT_ADS.json contains an initial/current gallery sample of up to 100 observed ads. Check snapshotTruncated before treating the preview as exhaustive; use the referenced historical observations Dataset for the retained records. The first persistent BASELINE_ONLY run creates a reference without treating every existing ad as new.

SUMMARY.json includes crossChannelInsights and agencyBrief. Creative families describe observed relationships in copy, offers, destination and media, with supporting source evidence; they are not proof of shared budgets, ownership or performance. Compare the source timestamps before presenting an apparent cross-channel expansion to a client.

The agency brief covers the interval since the preceding comparable scan. For a weekly client comparison, use a weekly Apify Schedule. A daily scan does not automatically become a seven-day report. Changing country, target identity or source filters establishes a fresh comparison scope; check scope-reset diagnostics rather than mixing incompatible markets or advertisers.

A partial or truncated first observation can establish a reference for observed ads and later positive changes. Only complete scans advance missing-ad counters. Unavailable landing-page enrichment is diagnosed separately and does not erase the last valid landing fingerprint.

No-code activation

The project includes workflows/n8n-competitor-ad-monitor.json and workflows/make-competitor-ad-monitor.json, with setup instructions in workflows/README.md. They begin in DATASET dry-run mode and have no active schedule. Add an Apify authorization credential, your confirmed advertiser identity and a readable source Dataset before running. Route change records by recordType and deduplicate by eventId.

Cost calculator

python scripts/estimate_cost.py --competitors 10 --channels 2 --runs 30

This prints 600 checks and $15.00 in monitoring-event charges at the default illustrative rate of $0.025/check. Pass --unit-price if the Store rate changes and --source-cost only when you know the independent source total. Unknown source cost is reported as null, not zero. The calculation does not start an Actor or reserve credits.

Pay-per-event and source costs

The implementation uses one custom event: advertiser-channel-check. A check is chargeable only when its result is usable: either a complete verified-zero response or a response containing normalized ads. Empty partial, blocked, unsupported, and failed checks are not charged by this custom event.

Pricing is controlled by the live Pricing tab in Apify Store; the repository does not guarantee a price. Apify platform usage can also apply. Compatible child source Actors are billed separately under their own Store listings and remain subject to maxSourceChargeUsd and the run-level maximum charge you configure.

dryRun disables this Actor's custom event, but child source Actors can still charge. Use DATASET mode with existing datasets when you want to validate without starting child Actors.

Privacy, retention, and responsible use

  • Source datasets are opened with read-only permission and are not modified.
  • Public transparency records can contain creative text and media URLs, landing URLs, advertiser or payer names, creator identity, targeting attributes, and impression ranges. Normalized evidence can retain those fields.
  • Landing-page requests disclose a normal network request to the destination site.
  • Webhook inputs are marked secret. The signing secret is not written to datasets, reports, or returned delivery diagnostics.
  • Persistent monitors use named Apify storage for state, normalized observations, and diagnostics. Named storage remains until you delete it; default run storage follows your Apify plan and retention settings. See Apify storage retention.
  • dryRun suppresses persistent monitor-state mutations and alert delivery, but it can still read selected datasets and can still start separately billed child Actors outside DATASET mode.
  • Delete the monitor's named ad-intel-...-state, ad-intel-...-ads, and ad-intel-...-diagnostics storages when you no longer need the history.
  • You are responsible for a lawful basis, data-protection obligations, platform terms, source-Actor terms, and access restrictions. Public availability does not grant copyright or redistribution rights over third-party creatives.

This tool supports transparency and competitive research. It is not legal advice and is not a substitute for a DSA, GDPR, copyright, or platform-terms assessment.

Beta checklist before relying on a monitor

  • Confirm that each target maps to the correct advertiser using stable platform identifiers.
  • Inspect source-health warnings and skipped-record counts after every schema or provider change.
  • Compare a sample of normalized records with the original transparency-library pages.
  • Keep BASELINE_ONLY for the first persistent run unless you intentionally want existing ads emitted as new.
  • Treat a zero-result scan as meaningful only when health is FULL.
  • Pin and retest compatible source Actor versions before production schedules.
  • Deduplicate downstream events by eventId and summaries by recordId.
  • Monitor source costs and the run-level maximum charge.

If a run looks incomplete, inspect the final RUN_SUMMARY and SUMMARY.json before acting. They are designed to make missing coverage visible rather than hide it behind an empty result.

Browse Programadores.com on Apify for complementary company-enrichment, website-technology, Spanish CIF/NIF, web-to-Markdown and ad-monitoring tools. Each Actor has its own input contract and pricing. For reproducible extraction issues, use this Actor's Issues tab and include a public sample input and the affected field; do not post API tokens or private datasets.