# GDPR Cookie Consent Verifier (Cookie Banner Checker) (`mrdexter4/cookie-consent-verifier`) Actor

Test cookie banners like EU regulators do: each page is loaded before consent, after Reject and after Accept. Flags trackers before consent, tracking after reject, missing reject button, pre-ticked boxes and Consent Mode v2. EN/NL HTML + PDF report.

- **URL**: https://apify.com/mrdexter4/cookie-consent-verifier.md
- **Developed by:** [Mateus Correia](https://apify.com/mrdexter4) (community)
- **Categories:** Developer tools, Marketing, SEO tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $70.00 / 1,000 page checkeds

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

## GDPR Cookie Consent Verifier: cookie consent checker with before / reject / accept evidence

Check whether a **GDPR cookie banner** really works. The test is modelled on the before / reject / accept method EU data-protection authorities use in cookie-banner investigations: each page is loaded in three fresh browsers, **before any choice**, **after Reject** and **after Accept**. You get cookies, localStorage items and tracker requests per state, seven pass / warn / fail checks, fix hints and an HTML and PDF report in English or Dutch.

**No subscription, no auto-renew, no domain or page-view tiers.** $0.10 per page checked, $0.50 per site report.

**Use cases**

- **Agencies and freelancers:** check a client's banner after every CMP or tag-manager change and attach the PDF.
- **Small e-commerce and SaaS teams:** confirm Reject really blocks analytics and marketing tags, then monitor weekly.
- **Privacy consultants and DPOs:** collect consent verification evidence for many sites before manual review.
- **Developers and analytics teams:** check Consent Mode v2 from CI, n8n, Make or Zapier.
- **Dutch market:** reports in Dutch with references to the Autoriteit Persoonsgegevens' guidance ("Heldere cookiebanners").

**Sample output** (shortened, real run on our demo shop):

```json
{
  "type": "page",
  "url": "https://cookie-demo.mateuscorreia.com.br/",
  "verdict": "fail",
  "visitorCountry": "NL",
  "checks": {
    "no-tracking-before-consent": "fail",
    "no-tracking-after-reject": "fail",
    "reject-as-prominent": "warn",
    "no-pre-ticked-boxes": "fail"
  },
  "counts": { "nonEssentialCookiesBeforeConsent": 2, "nonEssentialCookiesAfterReject": 2 }
}
```

> This is an automated technical test, **not legal advice**. Findings are worded as *potential issues*. Results depend on location, browser, timing and A/B tests. Have a privacy professional confirm important findings.

### What does the GDPR Cookie Consent Verifier do?

- **Three fresh browser sessions per page**, so nothing leaks between states. The before state is measured on first load; Reject and Accept are measured after a reload, when many tags fire.
- **Detects the consent platform (CMP):** Cookiebot, OneTrust, Complianz, CookieYes, Didomi, Usercentrics and 12 more, plus custom banners, inside iframes and shadow DOM too.
- **Finds the buttons by label** in seven main EU languages and by known CMP selectors, or by your own CSS selectors.
- **Classifies cookies** with the [Open Cookie Database](https://github.com/jkwakman/Open-Cookie-Database) and **tracker requests** with [EasyPrivacy](https://easylist.to/).
- **Decodes Google Consent Mode v2** (`gcs` / `gcd`), so a "denied" cookieless ping is a warning, not full tracking.
- **Monitor mode** outputs only pages that changed since the previous run.
- **Several sites per run**, with a combined report and site summaries. Export to CSV, Excel or JSON.

### The seven checks

| Check | What fails it | Based on |
|---|---|---|
| `banner-present` | Tracking seen but no consent banner found | ePrivacy Directive Art. 5(3) |
| `no-tracking-before-consent` | Analytics/marketing cookies or tracker requests before any choice | ePrivacy Art. 5(3), EDPB Guidelines 2/2023 |
| `reject-on-first-layer` | "Accept" on the first layer but no equivalent "Reject" | EDPB Cookie Banner Taskforce report §3 |
| `no-tracking-after-reject` | Tracking cookies or tracker requests still present after "Reject" | ePrivacy Art. 5(3), EDPB 05/2020, Taskforce §8 |
| `reject-as-prominent` *(heuristic, warn only)* | Reject clearly less visible than Accept | Taskforce §5–6 |
| `no-pre-ticked-boxes` | Non-essential purposes switched on by default | CJEU C‑673/17 *Planet49*, Taskforce §4 |
| `consent-mode-v2` | Google tags "granted" before consent (fail); missing v2 signals or still "denied" after Accept (warn) | Google EU user consent policy (Google policy, not law) |

A **warn** marks findings that need judgement, such as cookieless pings or unclassified third-party cookies. **n/a** means a check did not apply, for example `consent-mode-v2` when no Google tags were seen. A page that could not be loaded gets `n/a` as its verdict.

**Location matters.** If `visitorCountry` is outside Europe, every *fail* becomes *warn*, because many CMPs only show a banner to EU visitors.

### Input

| Field | Description | Default |
|---|---|---|
| `startUrls` | Pages to check. URLs on one domain are grouped as one site. | – |
| `maxPagesPerSite` | Pages per site; extra pages are found via links on the first page | 1 |
| `proxyConfiguration` | Use an EU country: banners are often geo-targeted | Apify residential, NL |
| `locale` | Browser language and region (`nl-NL`, `de-DE`, `en-GB`, …) | `nl-NL` |
| `language` | Report language: `en` or `nl` | `en` |
| `generateReport` / `generatePdf` / `includeScreenshots` | HTML report, PDF copy, banner screenshots | `true` |
| `monitorMode` / `monitorStoreName` / `webhookUrl` | Output only changes since the previous run with the same store name; optional webhook | `false` |
| `customBannerSelector`, `customAcceptSelector`, `customRejectSelector`, `customSettingsSelector` | CSS selectors for banners the detection misses | – |
| `viewport` | `desktop` (1366×900) or `mobile` (390×844) | `desktop` |
| `blockImagesAndMedia`, `maxConcurrency`, `pageTimeoutSecs` | Advanced. Tracker images are never blocked. | `true`, 2, 45 |

**Try it on the demo shop.** The prefilled URL, `https://cookie-demo.mateuscorreia.com.br/`, is a fictional shop with a *deliberately non-compliant* banner: cookies before consent and after Reject, a small grey Reject link and pre-ticked options. Its `/compliant` page passes every check. No real tracking services are loaded.

### Output

**Dataset:** one row per page (`type: "page"`) plus one per site (`type: "site-summary"`). Page rows also contain `cmpVendor`, `checkDetails` (evidence, fix and references per check), `banner`, `consentMode` and `states.before / reject / accept`.

**Key-value store:** `report.html`, `report.pdf`, banner screenshots, `SUMMARY` and `OUTPUT`.

### Schedule recurring consent checks

Consent often breaks after launch, when someone adds a pixel or a CMP update changes a trigger.

1. Save your sites and settings as an Apify **Task**, one per client with its own `monitorStoreName`.
2. Turn on `monitorMode` and, optionally, set `webhookUrl`.
3. In Apify Console, open **Schedules** and add the task with a cron expression, for example `0 8 * * 1` for Mondays at 08:00.

The first run is the baseline: every page comes through as `changeType: "new"` and is sent to the webhook. After that, each run outputs only pages that changed, with a `changes` list, and the webhook fires only when something changed. See [Apify schedules](https://docs.apify.com/actors/running/schedules).

### Use it from n8n, Make and Zapier

Apify has official integrations for all three. Connect your Apify account with an API token, then use the Apify app or node to **run an Actor or Task** and **fetch its dataset items**, or start a workflow **when a run finishes**.

Typical workflows:

- **Alert on a broken banner:** schedule a run with `monitorMode` off, so every page is in the dataset, then filter page rows with `verdict: "fail"` and post them to Slack or email. (With `monitorMode` on, a page that keeps failing is not output again.)
- **Ticket on change:** create a Jira or Trello card for each row with `changeType: "changed"`, with the report link from `OUTPUT`.

Guides: [n8n](https://docs.apify.com/integrations/n8n), [Make](https://docs.apify.com/integrations/make), [Zapier](https://docs.apify.com/integrations/zapier).

### Pricing (pay per event)

| Event | Price |
|---|---|
| Page checked (`page-checked`), three browser sessions per page | **$0.10 per page** |
| Report generated (`report-generated`) | **$0.50 per site report** (HTML + PDF) |

Examples: one homepage with a report costs **$0.60**. 20 sites × 3 pages with reports cost **$16.00**. A weekly check of one homepage with a report costs about **$2.60 a month**.

Failed pages are not charged. Set **Maximum cost per run** to cap spend; the Actor stops cleanly at the limit.

### FAQ

**Does a pass mean my site is GDPR compliant?**
No. It tests what a browser can observe on the pages you give it. It does not check your privacy policy, purposes, vendors or consent records. Use it to find and monitor problems, not as a certificate.

**Why does it report tracking after Reject when my CMP blocks everything?**
Usually tags hard-coded outside the CMP, tag-manager triggers that ignore consent, cookies set earlier and not deleted, or services listed as "necessary". The evidence names each cookie and request.

**Is a Google Consent Mode "denied" ping a violation?**
It is reported as a **warn**. With Consent Mode "advanced", Google tags send cookieless pings before consent. EDPB Guidelines 2/2023 say these can still fall under Art. 5(3) ePrivacy; authorities have no single line yet. The most conservative option is Consent Mode "basic".

**Why was no banner found?**
The site shows no banner to the tested location (use an EU proxy), the banner appeared late, or detection missed it. Set `customBannerSelector` and the button selectors; `banner.diagnostics` shows what the scanner saw.

**Why did a page fail?**
Bot protection or a timeout. Failed pages are not charged. Retry with the residential proxy or a longer `pageTimeoutSecs`.

### Limitations

- `reject-as-prominent` is a heuristic. Regulators judge prominence case by case, so it only warns.
- Consent Mode `gcd` decoding is based on community documentation, not an official Google spec.
- Purposes, IAB TCF strings, withdrawal of consent and the cookie policy are not checked.
- Cookie categories come from public databases; unknown cookies are marked *Unknown*, not as violations.
- Server-side tracking, fingerprinting without network traces and native apps are not covered.
- Banners that appear only after scrolling, after more than about 20 seconds, or after a login are not tested.
- Some report evidence strings (cookie details, request types) stay in English in the Dutch report.

### Data sources and credits

- **Open Cookie Database** by the Open Cookie Database contributors (maintained by jkwakman), Apache License 2.0: https://github.com/jkwakman/Open-Cookie-Database
- **EasyPrivacy** by The EasyList authors (https://easylist.to/), dual-licensed GPLv3 / CC BY-SA 3.0. The Actor uses its domain entries to classify requests.

See `data/ATTRIBUTION.md` for details.

Licence texts: [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0), [GPLv3](https://www.gnu.org/licenses/gpl-3.0.html), [CC BY-SA 3.0](https://creativecommons.org/licenses/by-sa/3.0/).

### Keywords

cookie consent checker, GDPR cookie banner checker, consent verification, ePrivacy compliance, cookie audit, consent mode v2 checker, Cookiebot / OneTrust test, cookiebanner controle, AVG cookies, Autoriteit Persoonsgegevens cookiebanner.

# Actor input Schema

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

One or more page URLs (usually the homepage). Each distinct domain gets its own summary. The prefilled URL is a fictional demo shop with a deliberately non-compliant banner (its /compliant page shows a correct one).

## `maxPagesPerSite` (type: `integer`):

1 = only the start URL(s). Higher values also test internal pages linked from the first page (a different template can load different tags). Each page is charged as one 'page checked' event.

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

Strongly recommended: Apify Proxy with an EU country (e.g. NL, DE, FR). Without a European IP many consent platforms hide the banner or apply US rules, and the results are only indicative (failures are then reported as warnings). Every row reports the country the site saw (visitorCountry).

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

Browser locale (Accept-Language and navigator.language), e.g. nl-NL, de-DE, fr-FR, en-GB. Banner buttons are recognised in EN, NL, DE, FR, ES, IT and PT.

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

Language of the findings, fix guidance and HTML/PDF report.

## `generateReport` (type: `boolean`):

Client-ready HTML report (report.html in the key-value store) with screenshots, findings, fix guidance and sources.

## `generatePdf` (type: `boolean`):

PDF version of the report (report.pdf).

## `includeScreenshots` (type: `boolean`):

Save a screenshot of each page before consent (embedded in the report for runs up to 25 pages).

## `monitorMode` (type: `boolean`):

Compare with the previous run (stored in a named key-value store) and output only pages whose result, trackers or non-essential cookies changed. Use with a schedule. The first run outputs every page as 'new'.

## `monitorStoreName` (type: `string`):

Named key-value store with the previous results. Use different names to keep separate baselines (e.g. per client).

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

Optional. In monitor mode, a JSON summary of changes is POSTed here (Slack/Zapier/Make/n8n compatible).

## `customBannerSelector` (type: `string`):

Optional CSS selector of the cookie banner, for home-made banners that are not recognised automatically.

## `customAcceptSelector` (type: `string`):

Optional CSS selector of the accept button.

## `customRejectSelector` (type: `string`):

Optional CSS selector of the reject / only-necessary button.

## `customSettingsSelector` (type: `string`):

Optional CSS selector of the button that opens the preferences layer.

## `viewport` (type: `string`):

Desktop (1366×900) or mobile (412×915) browser.

## `blockImagesAndMedia` (type: `boolean`):

Faster and cheaper. Requests to tracker domains (including tracking pixels) are never blocked.

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

Pages tested at the same time (each uses three browser contexts one after another).

## `pageTimeoutSecs` (type: `integer`):

How long to wait for a page to load before giving up.

## Actor input object example

```json
{
  "startUrls": [
    {
      "url": "https://cookie-demo.mateuscorreia.com.br/"
    }
  ],
  "maxPagesPerSite": 1,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "NL"
  },
  "locale": "nl-NL",
  "language": "en",
  "generateReport": true,
  "generatePdf": true,
  "includeScreenshots": true,
  "monitorMode": false,
  "monitorStoreName": "cookie-consent-monitor",
  "viewport": "desktop",
  "blockImagesAndMedia": true,
  "maxConcurrency": 2,
  "pageTimeoutSecs": 45
}
```

# Actor output Schema

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

Report with per-page findings, screenshots, cookies and trackers per state, fix guidance and sources.

## `reportPdf` (type: `string`):

No description

## `pages` (type: `string`):

One row per page with pass/warn/fail per check, evidence, cookies and tracker requests per state.

## `summary` (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://cookie-demo.mateuscorreia.com.br/"
        }
    ],
    "proxyConfiguration": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ],
        "apifyProxyCountry": "NL"
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("mrdexter4/cookie-consent-verifier").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://cookie-demo.mateuscorreia.com.br/" }],
    "proxyConfiguration": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
        "apifyProxyCountry": "NL",
    },
}

# Run the Actor and wait for it to finish
run = client.actor("mrdexter4/cookie-consent-verifier").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://cookie-demo.mateuscorreia.com.br/"
    }
  ],
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "NL"
  }
}' |
apify call mrdexter4/cookie-consent-verifier --silent --output-dataset

```

## MCP server setup

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

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/amzHpax83mWJVBAYc/builds/1s2INYjDQR1N58akJ/openapi.json
