# SiteVerdict — Consent Mode v2 & Tracking Auditor (`siteverdict/siteverdict-tracking-consent-auditor`) Actor

Test if GA4, GTM, Google Ads & Meta Pixel fire before consent or after Reject. Loads each site 3 ways (no click, Reject, Accept), decodes Consent Mode v2, finds broken and duplicate tags, scores each site with plain-English fixes.

- **URL**: https://apify.com/siteverdict/siteverdict-tracking-consent-auditor.md
- **Developed by:** [Aman Thakran](https://apify.com/siteverdict) (community)
- **Categories:** Business, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per event

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

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

## SiteVerdict — Cookie Consent & Consent Mode v2 Auditor

**Prove whether "Reject" really stops tracking, with evidence your client can read.**

The Actor opens each website three times in real Chrome: no click, then **Reject**, then **Accept**. After each click it reads back the choice the cookie tool actually stored. You get:

- what fired before consent and after Reject, with timestamps;
- a Google Consent Mode v2 pass/fail checklist;
- a white-label **PDF evidence report** with screenshots, ready to send.

> **Indicators, not legal advice.** These are automated technical risk indicators for UK GDPR / PECR and EU ePrivacy, from one browser at one time. A qualified adviser should review them before you rely on them.

### Why this one

Most cookie checkers on the Store list cookies or detect a banner. Very few click **Reject**, and none we found **checks that the click was recorded** before blaming the site. That check is what keeps false positives out of a client report.

| | SiteVerdict | Typical cookie / consent checker |
|---|---|---|
| Clicks **Reject** and tests what still fires | ✅ (incl. "Reject" behind a settings layer) | ❌ or accept only |
| **Confirms the click was recorded** (reads the CMP / TCF / Consent Mode state back) | ✅ — findings carry `confidence`; unconfirmed ones are "check manually", never scored | ❌ |
| What **sent data** (network) vs a script that merely loaded | ✅ | ❌ (cookie lists / script presence) |
| Google **Consent Mode v2** decoded (`gcs`/`gcd`) + pass/fail checklist | ✅ | ❌ or "signals present" |
| CMPs | OneTrust, Cookiebot, Didomi, Usercentrics, Quantcast/InMobi, CookieYes, iubenda, Termly, Osano, TrustArc, Complianz, **Google CMP**, Sourcepoint, Civic, **Transcend (closed shadow DOM)** + 10 more + custom banners | 3–12 |
| Client-ready **PDF** with screenshots + timestamped request log | ✅ white-label | ❌ |
| **Monitoring** (diff vs last run) + Slack / webhook alerts | ✅ | ❌ |
| **AI fix plan** with your own key (exact CMP/GTM change + policy wording) | ✅ optional | ❌ |
| Bot-protection pages | reported as `blocked`, **not charged** | often scored as "clean" |

See **[Benchmark](#benchmark-25-real-ukeu-sites)** below and `BENCHMARK.md` for the method and full results.

### Three ways customers use it

1. **Agencies.** Run 1–200 client sites and hand over a branded PDF per site. Each PDF has banner screenshots, what fired before and after the choice, the Consent Mode checklist and the exact fix for their consent tool. Re-run after the fix to prove it worked.
2. **Marketing and analytics teams.** Catch the expensive bugs:
   - "Accept doesn't switch Google tags to granted", so conversions are lost;
   - the Consent Mode default is set **after** the first hit;
   - a GA4 ID returns 404;
   - duplicate `page_view`;
   - legacy Universal Analytics is still installed.
3. **DPOs, privacy consultants and lawyers.** Schedule a weekly monitor across your portfolio. You get a Slack or webhook alert the day a new tracker starts firing before consent or after Reject. A dated, timestamped evidence pack is attached for the file.

### Before / after: what you get

**A typical "checker" output:**

```text
banner: yes   cmp: OneTrust   cookies: 46 (22 first-party, 24 third-party)   score: 7/10
```

**SiteVerdict output for the same kind of site** (a real benchmark result from a large UK retailer, anonymised and shortened):

```json
{
  "url": "https://www.uk-retailer.example/",
  "score": 53, "grade": "D",
  "cmp": { "name": "OneTrust" },
  "scenarios": {
    "reject": { "interaction": { "buttonText": "Reject all cookies" }, "verification": "confirmed" }
  },
  "findings": [
    { "severity": "high", "confidence": "high", "title": "Tracking continues after the visitor clicks Reject",
      "evidence": ["[reject / after click] TikTok Pixel C… (EngagedSession): https://analytics.tiktok.com/api/v2/pixel"] },
    { "severity": "medium", "confidence": "high", "title": "Analytics tags send data before consent",
      "evidence": ["[no-consent] Google Analytics 4 G-XXXXXXX (page_view): https://region1.analytics.google.com/g/collect?v=2&tid=G-XXXXXXX&…&gcd=13l3l3l3l1l1&…"] }
  ],
  "consentMode": { "version": "v2", "checks": [ { "label": "Default consent set before the first Google hit", "status": "fail" } ] },
  "evidence": { "reportPdfUrl": "…/client-report-uk-retailer.pdf", "requestLogCsvUrl": "…/evidence-uk-retailer.csv" }
}
```

`gcd=13l3l3l3l1l1` means the hit was sent before any consent default existed. The checklist turns that into a plain "fail", and the fix guide gives the exact change for that site's consent tool. The PDF puts it on one page for the client.

### Benchmark (25 real UK/EU sites)

Run on 11 Oct 2026 against 25 public sites across news, retail, SaaS/fintech, government and travel. Each site was visited once per scenario, politely, after a `robots.txt` check. A further 12 sites run by consent-tool vendors were used for CMP coverage. Every finding was reviewed by hand against its evidence.

| Result (25 sector sites, manual review of every finding) | SiteVerdict 1.0 | **SiteVerdict 2.0** | Checkers that never click Reject (documented) |
|---|---|---|---|
| False positives (non-info findings) | 8 of 57 | **0 confirmed** of 56 (1 unverified; 3 shown as "check manually", not scored) | n/a |
| Bot-protection pages scored as "clean" | 2 | **0** (reported `blocked`, not charged) | not handled |
| Consent banners missed | 1 (closed shadow DOM) | **0** | — |
| Reject choice confirmed by the consent tool | — | **12 confirmed, 7 likely**, 1 unconfirmed, 2 consent-or-pay correctly not clicked | not available |
| Sites in the set with tracking after Reject that were caught | 4 | **5** (2nd-layer Reject now reached; with click-relative timestamps + confidence) | **0 possible** (no Reject click) |
| Avg time per site (3 visits + 1 inner page, PDF included) | 44 s | 53 s on a heavily loaded test server | 2–30 s (single load) |

Competitor Actors were **not run** (no spend on other developers' Actors). They are compared on their documented inputs and outputs. Method, the site list, per-site results and caveats are in `BENCHMARK.md`.

### Pricing (pay per event)

| Event | When | Price |
|---|---|---|
| `site-audited` | A site was fully audited (no click / Reject / Accept) | **$0.19** |
| `evidence-pack` | Client PDF + HTML, after-click screenshots and request log saved (on by default) | **$0.06** |
| `extra-page-audited` | Each extra internal page (all scenarios) | $0.04 |
| `quick-scan` | First-visit-only scan (no clicks) | $0.03 |
| `ai-fix-generated` | AI fix plan produced with **your own** API key (off by default) | $0.04 |
| `alert-sent` | Slack / webhook digest delivered | $0.01 |

**Examples:**

| Job | Cost |
|---|---|
| 1 site, full audit + PDF | **$0.25** |
| 100 sites + PDFs | **≈ $25** |
| Weekly monitoring of 20 sites with Slack alerts, no PDFs | **≈ $15/month** |
| 1,000-site quick prospecting scan | **$30** |

Not charged:

- sites that fail to load;
- sites behind bot protection (`blocked`);
- AI or alert steps that do not deliver.

Set a maximum total charge per run and the Actor stops cleanly when it is reached.

### Input

```json
{
  "startUrls": [{ "url": "https://www.example.co.uk" }],
  "scanMode": "full",
  "evidencePack": true,
  "reportAgencyName": "Your Agency Ltd",
  "reportClientName": "Example Ltd",
  "maxInternalPages": 1,
  "monitoringMode": false,
  "aiFix": false
}
```

**Monitoring:** set `monitoringMode: true` and a `monitorName` (one per client), then schedule the Actor. Each item gets a `change` object, for example:

```json
"change": { "changed": true, "headline": "NEW before consent: Meta Pixel · 1 new issue(s): Advertising tags send data before consent" }
```

Add `slackWebhookUrl` or `webhookUrl` (both stored as secrets) to get one digest per run. `onlyChanged: true` keeps the dataset to the sites that changed.

**AI fix (optional):** set `aiFix: true`, `aiProvider` (`anthropic` or `openai`) and `aiApiKey` (your key, stored as a secret, never logged). Only the audit summary is sent: findings, tag names and IDs, the Consent Mode state and the CMP name. No cookies and no request log are sent. The plan covers:

1. CMP changes
2. GTM / tag changes
3. A Consent Mode v2 snippet
4. Cookie-policy wording
5. How to verify

It is embedded in the PDF and saved as Markdown.

### Output

- **Dataset**, one item per site. Views:
  - **Overview**
  - **Client reports & evidence**
  - **Consent Mode v2 checks**
  - **Findings** (with `confidence`)
  - **Tracking IDs**
  - **Changes (monitoring)**
- **Key-value store, per site:**
  - `client-report-<site>.pdf` and `.html`
  - `evidence-<site>.csv` and `.json`: the redacted request log with times relative to the click
  - screenshots before the choice, after Reject and after Accept
  - `ai-fix-<site>.md` (if AI fix is on)
- **Key-value store, run level:** `REPORT` (HTML for all sites) and `SUMMARY` (agent-ready JSON).

**How findings are graded:**

| Field | Values |
|---|---|
| Severity | critical / high / medium / low / info |
| Confidence | **high**: the consent tool confirmed the choice; **medium**: the banner closed but the state could not be read; **low**: check manually, not scored |

**Scores:** consent behaviour and tag health, each 0–100, plus the overall score and a grade.

### How accuracy is protected

These are the false-positive guards, each covered by tests:

- **Choice read-back** after every click, from these sources:
  - CMP cookies and APIs: OneTrust (by group *name*), Cookiebot, CookieYes, Didomi, Usercentrics, Osano, TrustArc, iubenda, Complianz, Transcend;
  - IAB TCF strings and the Google CMP;
  - GOV.UK / NHS-style JSON cookies;
  - the Google Consent Mode state.
- **Wrong-click guards.** It never clicks "More options" as Reject or "Accept only essential" as Accept, and it ignores accordion headers in preference centres.
- **Only data actually sent counts.** These do not count:
  - script downloads;
  - cookieless Consent Mode or Microsoft UET (`asc=D`) pings;
  - Google `npa=1` / limited-ads requests, which get their own low-severity note;
  - beacons already in flight when the button was clicked.
- **Cookie names are matched on the vendor's own domain.** A first-party `uid` is not Criteo.
- **Late banners** are waited for. **Closed shadow DOM** banners are handled. **Consent-or-pay** banners are recognised and the paid option is never clicked.
- **Bot-protection pages** are reported as `blocked`, not as a clean site.

### FAQ

**Does it log in, submit forms or get around bot protection?**
No. It loads public pages and clicks cookie-banner buttons, the way a visitor would. It never solves CAPTCHAs or bypasses protection. If a site shows a bot check, the site is marked `blocked` and is not charged.

**Why does a finding say "check manually"?**
Either the consent tool did not confirm the Reject/Accept click, or a check depends on something a browser cannot see, such as server-side tagging. Those findings are shown but never scored.

**Is analytics before consent always a breach?**
Not necessarily. The UK Data (Use and Access) Act 2025 created a narrow exemption for some statistics cookies, and some EU regulators (for example the CNIL) exempt strictly configured audience measurement. That is why analytics-only issues are rated *medium* and advertising or session-replay issues *high*. Ask your adviser.

**Can I white-label the PDF?**
Yes. Set `reportAgencyName` and `reportClientName`.

**Which country's banner do I get?**
Banners are often geo-targeted. Use Apify Proxy with your visitors' country (GB, DE, FR…), and set the browser language and time zone.

### Limits

- One snapshot from one location and device profile (desktop Chrome). Banners and tags vary by country, A/B test and time.
- Tags that wait for scrolling or another interaction may not fire. Server-side tagging is only visible as far as the browser sees it.
- WebSocket traffic, used by some session-replay tools, is not captured. A replay script that loaded but showed no data is reported as low severity.
- It does not judge your banner wording, colours or legal basis.

### Compliance and sources

- **Personal data:**
  - The Actor collects no personal data about individuals.
  - Request logs keep only tag parameters (`tid`, `en`, `gcs`, `gcd`, `id`, `ev`…). Client IDs, user IDs, page paths, referrers and titles are redacted.
  - Cookie *values* are never stored. Consent-cookie values are parsed in memory and only the derived yes/no is kept.
- **Your API key and webhooks:** the AI key and webhook URLs are secret inputs. They are never logged or output.
- **Sources:**
  - ICO, storage and access technologies guidance (2026): https://ico.org.uk/for-organisations/direct-marketing-and-privacy-and-electronic-communications/guidance-on-the-use-of-storage-and-access-technologies/
  - Data (Use and Access) Act 2025: https://www.gov.uk/guidance/data-use-and-access-act-2025-plans-for-commencement
  - Google Consent Mode: https://developers.google.com/tag-platform/security/guides/consent
  - EDPB Cookie Banner Taskforce report (17 Jan 2023)
  - IAB TCF v2.2 technical specification
  - `gcs`/`gcd` meanings follow public reverse-engineering write-ups and are not official Google documentation.

**Disclaimer:** these are automated technical risk indicators, **not legal advice**, and not a certification of compliance with UK GDPR, PECR, EU GDPR or the ePrivacy Directive.

***

Made by **SiteVerdict** (BharatFare Travel and Technologies Ltd, UK). Pair it with the **SiteVerdict Cookie & Privacy Policy Checker**, which checks that your cookie and privacy policies match what the site really sets and drafts the missing wording. Contact: Use the Issues tab on this Actor's Store page.

# Changelog

This Actor's version history is a separate document: https://apify.com/siteverdict/siteverdict-tracking-consent-auditor/changelog.md

# Actor input Schema

## `startUrls` (type: `array`):

Home pages (or any landing pages) to audit. One dataset item and one score per URL. A missing https:// is added automatically.

## `scanMode` (type: `string`):

Full = 3 browser visits per site (no interaction, Reject, Accept all) - tests what happens after each consent choice. Quick = 1 visit (no interaction only): tag inventory, pre-consent tracking, cookies and broken tags, at a lower price.

## `evidencePack` (type: `boolean`):

For each audited site, save a client-ready report (HTML + PDF) with screenshots of the banner and of the page after Reject / Accept, the Consent Mode v2 checklist, CMP-specific fix steps and a timestamped, redacted request log (CSV + JSON) showing what fired before and after each choice. Billed as 'evidence-pack' only when it is produced (full scans of sites that could be audited). Full audits only (quick scans never produce or charge it).

## `reportAgencyName` (type: `string`):

Shown at the top of the client report ("<your name> · powered by SiteVerdict"). Leave empty for SiteVerdict branding.

## `reportClientName` (type: `string`):

Optional client name for the report title.

## `maxInternalPages` (type: `integer`):

Also visit up to this many internal pages (found from home-page links, same host) in each scenario, to check tags on deeper pages and that consent persists across navigation. 0 = home page only. Each extra page is billed as 'extra-page-audited'.

## `maxConcurrency` (type: `integer`):

How many sites are audited in parallel. Each site uses its own browser context. 3 is good for 2 GB memory; raise memory when you raise this.

## `settleTimeSecs` (type: `integer`):

Extra time to wait after the page loads (and after each click) so that delayed tags and cookie banners can appear.

## `bannerWaitSecs` (type: `integer`):

If no banner is visible after the page settles, keep checking for up to this long before concluding there is none. Prevents false 'no banner' results on lazy-loading consent tools.

## `navigationTimeoutSecs` (type: `integer`):

Maximum time to wait for each page to load before the scenario is marked as failed.

## `saveScreenshots` (type: `boolean`):

Store a JPEG screenshot of what a first-time visitor sees (usually the cookie banner) in the key-value store, linked from each result.

## `locale` (type: `string`):

Browser locale (Accept-Language). Banners are often localised; en-GB is the default.

## `timezoneId` (type: `string`):

IANA time zone, e.g. Europe/London or Europe/Berlin.

## `userAgent` (type: `string`):

Leave empty to use a standard desktop Chrome user agent. Note: some consent tools hide the banner for crawler-like user agents.

## `saveRawCapture` (type: `boolean`):

Also store the tag-related network requests of every scenario (identifiers redacted) as JSON in the key-value store (record capture-<site>). Useful for debugging or your own analysis.

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

Optional. Cookie banners and tags are often geo-targeted: use Apify Proxy with country GB, DE, FR etc. to see what visitors from that country get.

## `monitoringMode` (type: `boolean`):

Keep a snapshot of each site in a named key-value store and report what changed since the previous run of the same monitor: new trackers before consent or after Reject, new / resolved issues, consent tool or Consent Mode changes. Schedule the Actor (e.g. daily or weekly) to monitor sites continuously.

## `monitorName` (type: `string`):

Name of this monitor (e.g. a client name). Snapshots are stored in the key-value store 'siteverdict-consent-monitor-<name>' in your account, so different monitors do not mix.

## `onlyChanged` (type: `boolean`):

In monitoring mode, push a dataset item only for sites that changed (and on the first run). Every site is still audited and billed; the SUMMARY record lists all sites.

## `alertOn` (type: `string`):

'changes' (default in monitoring mode): alert when a monitored site changed. 'issues' (default otherwise): alert when a site has critical/high issues confirmed by the audit. One digest message per run and destination.

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

https://hooks.slack.com/... URL. Stored as a secret, never logged. Billed as 'alert-sent' per delivered message.

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

Any public HTTPS endpoint (Zapier, Make, n8n, your API). Receives a JSON digest. Stored as a secret, never logged. Billed as 'alert-sent' per delivered message.

## `aiFix` (type: `boolean`):

Off by default. Uses YOUR Anthropic or OpenAI key to draft the exact CMP / GTM changes, a Consent Mode v2 snippet and cookie-policy wording for each site with issues. Only the audit summary is sent (no cookies, no request log). Billed as 'ai-fix-generated' only when a plan is produced; the LLM cost is on your own key.

## `aiProvider` (type: `string`):

Which API your key belongs to.

## `aiApiKey` (type: `string`):

Your own Anthropic or OpenAI API key. Stored as a secret, never logged or output.

## `aiModel` (type: `string`):

Leave empty for the default (Anthropic: claude-sonnet-5-5, a mid-priced model; OpenAI: gpt-4.1-mini). Any model your key can use, e.g. claude-sonnet-5-5 for the strongest plans or claude-haiku-5-5 for the cheapest.

## Actor input object example

```json
{
  "startUrls": [
    {
      "url": "https://www.example.com"
    }
  ],
  "scanMode": "full",
  "evidencePack": true,
  "maxInternalPages": 1,
  "maxConcurrency": 3,
  "settleTimeSecs": 3,
  "bannerWaitSecs": 6,
  "navigationTimeoutSecs": 35,
  "saveScreenshots": true,
  "locale": "en-GB",
  "timezoneId": "Europe/London",
  "saveRawCapture": false,
  "proxyConfiguration": {
    "useApifyProxy": false
  },
  "monitoringMode": false,
  "monitorName": "default",
  "onlyChanged": false,
  "aiFix": false,
  "aiProvider": "anthropic"
}
```

# Actor output Schema

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

No description

## `report` (type: `string`):

No description

## `summary` (type: `string`):

No description

## `files` (type: `string`):

No description

# 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 = {
    "startUrls": [
        {
            "url": "https://www.example.com"
        }
    ],
    "proxyConfiguration": {
        "useApifyProxy": false
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("siteverdict/siteverdict-tracking-consent-auditor").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 = {
    "startUrls": [{ "url": "https://www.example.com" }],
    "proxyConfiguration": { "useApifyProxy": False },
}

# Run the Actor and wait for it to finish
run = client.actor("siteverdict/siteverdict-tracking-consent-auditor").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 '{
  "startUrls": [
    {
      "url": "https://www.example.com"
    }
  ],
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}' |
apify call siteverdict/siteverdict-tracking-consent-auditor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,siteverdict/siteverdict-tracking-consent-auditor"
        }
    }
}
```

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/s6qO1fSlWYgtyVLdp/builds/5STJhEh8v9nclc2fr/openapi.json
