Docs

Consent log, stats & export

AdminUpdated Sep 15, 2026

Consent log, stats & export

Every decision a visitor makes — accept, decline, or a granular save — is written to a tamper-evident, hash-chained consent log per site (the anonymized IP and, when encryption is configured, the choices themselves are stored so a subject's records can later be crypto-erased). These endpoints read that log in four shapes: chain-integrity check, aggregated stats, raw rows, and a CSV audit export — plus signed per-decision receipts and subject-level erasure/export for GDPR/CCPA requests.

All /v1/sites/:cbid/consent/... and /v1/sites/:cbid/{subject-export,erase-consent} routes require the consent scope: consent:read for GETs, consent:write for the POST that erases. This applies to subject-export and erase-consent as well — even though a sites-scoped key manages everything else about a site, it deliberately cannot read or destroy consent PII; you need a consent:read/consent:write grant for those two specifically. receipt is the one exception, with its own narrower scope — see the note at the end of this page.

GET /v1/sites/:cbid/consent/verify

Recomputes the hash chain for this site's consent log and reports whether it's intact — the tamper-evidence check itself, exposed as an API so you can alert on it instead of eyeballing the dashboard.

curl https://api.cookiemunch.net/v1/sites/acme-com-9f3k/consent/verify \
  -H "Authorization: Bearer fck_…" \
  -H "User-Agent: Mozilla/5.0"
{ "valid": true }

valid: false means a record's stored hash no longer matches what's computed from its content and the previous link — investigate immediately; this should never happen absent a bug or tampering.

GET /v1/sites/:cbid/consent/stats

Aggregated per-day totals — the fastest way to build a dashboard chart.

Query param

Type

Description

from

integer

Epoch-ms lower bound (inclusive). Defaults to 0.

to

integer

Epoch-ms upper bound (inclusive). Defaults to "now" (Number.MAX_SAFE_INTEGER internally).

curl "https://api.cookiemunch.net/v1/sites/acme-com-9f3k/consent/stats?from=1719792000000&to=1719878400000" \
  -H "Authorization: Bearer fck_…" \
  -H "User-Agent: Mozilla/5.0"
[
  {
    "Date": "2024-07-01",
    "OptIn": 842,
    "OptOut": 219,
    "OptInImplied": 0,
    "OptInStrict": 842,
    "TypeOptInPref": 601,
    "TypeOptInStat": 780,
    "TypeOptInMark": 512,
    "Impressions": 1150,
    "Countries": { "US": 640, "DE": 201, "GB": 220 }
  }
]

OptIn/OptOut count records where at least one non-necessary category was granted/declined; OptInImplied/OptInStrict split that by capture method; TypeOptInPref/Stat/Mark count per-category grants regardless of the overall decision. One object per UTC day with at least one record in range.

import { createCookieMunch } from '@cookiemunch/sdk';
const fc = createCookieMunch({ apiKey: process.env.FC_KEY, baseUrl: 'https://api.cookiemunch.net' });
const days = await fc.consent.stats('acme-com-9f3k', { from: 1719792000000, to: 1719878400000 });

GET /v1/sites/:cbid/consent/log

Recent anonymized consent records, most recent first.

Query param

Type

Description

from / to

integer

Epoch-ms range, same defaults as stats.

limit

integer

Max rows to return. Default 200, capped at 1000 server-side (a larger value is silently clamped).

curl "https://api.cookiemunch.net/v1/sites/acme-com-9f3k/consent/log?limit=50" \
  -H "Authorization: Bearer fck_…" \
  -H "User-Agent: Mozilla/5.0"
[
  {
    "stamp": "a1b2c3d4e5f6",
    "receivedAt": 1719858421000,
    "region": "de",
    "method": "explicit",
    "choices": { "preferences": true, "statistics": true, "marketing": false },
    "anonIp": "203.0.113.0",
    "url": "https://acme.com/pricing"
  }
]

stamp is the visitor's consent-receipt id — the same value CookieMunch.consentId() returns client-side — and the key you pass to the receipt, subject-export, and erase-consent endpoints below. method is "explicit" or "implied".

const rows = await fc.consent.log('acme-com-9f3k', { limit: 50 });

GET /v1/sites/:cbid/consent/export

The full audit trail as CSV — the artifact regulators ask for. Same from/to query params as stats/log (no limit; export is unbounded).

curl "https://api.cookiemunch.net/v1/sites/acme-com-9f3k/consent/export?from=1719792000000" \
  -H "Authorization: Bearer fck_…" \
  -H "User-Agent: Mozilla/5.0" \
  -o consent-log.csv
Consent ID,Date/Time (UTC),Anonymized IP,User Agent,URL,Region,Necessary,Preferences,Statistics,Marketing,Method,Version
a1b2c3d4e5f6,2024-07-01T18:07:01.000Z,203.0.113.0,Mozilla/5.0...,https://acme.com/pricing,de,true,true,true,false,explicit,1

Necessary is always true (strictly-necessary storage doesn't require consent). Response headers set Content-Type: text/csv and a Content-Disposition: attachment filename of consent-log.csv.

Note: export additionally requires the site to be domain-verified (verified: true on the site record) and returns 403 with { "error": "domain not verified" } otherwise. verify, stats, and log have no such requirement.

const csv = await fc.consent.export('acme-com-9f3k', { from: 1719792000000 });

GET /v1/sites/:cbid/receipt/:stamp

A signed consent receipt for one decision — portable proof of what was consented to, when, and under what notice version. Requires the site to be verified and receiptSecret configured on the server.

curl "https://api.cookiemunch.net/v1/sites/acme-com-9f3k/receipt/a1b2c3d4e5f6?format=json" \
  -H "Authorization: Bearer fck_…" \
  -H "User-Agent: Mozilla/5.0"
{
  "receipt": {
    "version": "1.0",
    "collector": "acme-com-9f3k",
    "consentTimestamp": "2024-07-01T18:07:01.000Z",
    "pii": false,
    "proofId": "a1b2c3d4e5f6",
    "region": "de",
    "purposes": [
      { "category": "necessary", "purpose": "Strictly necessary cookies and storage", "consentType": "explicit" },
      { "category": "statistics", "purpose": "Statistics and analytics measurement", "consentType": "explicit" }
    ]
  },
  "signature": "sha256:9f2a8b1e..."
}

Named purposes with a matching RoPA entry are enriched with lawful basis, data categories, recipients, and retention when the deployment has RoPA wired up; if an asymmetric signing key is configured, the response also carries a third-party-verifiable publicSignature/keyId. The format query parameter also accepts html (a printable document) and pdf (downloadable, also negotiable via Accept: application/pdf) for handing a visitor a document instead of raw JSON. 404 if there's no consent record for that stamp; 403 if the site isn't verified; 501 if receipts aren't configured on this deployment.

const { receipt, signature } = await fc.consent.receipt('acme-com-9f3k', 'a1b2c3d4e5f6');

Scope note: unlike the rest of this page, receipt accepts either receipt:read or consent:read — reading one receipt by its known stamp is a much narrower capability than exporting the whole log, so it has its own scope for an integration that should only ever render receipts.

Subject-level erasure & export

Two endpoints operate on every record for one stamp — the pattern for handling a GDPR/CCPA data-subject request for a specific visitor without going through the DSAR workflow manually. Both require the consent scope (read for export, write for erasure) — see the scope callout at the top of this page.

# Export a subject's own records (portability/access)
curl "https://api.cookiemunch.net/v1/sites/acme-com-9f3k/subject-export?stamp=a1b2c3d4e5f6" \
  -H "Authorization: Bearer fck_…" \
  -H "User-Agent: Mozilla/5.0"
{ "cbid": "acme-com-9f3k", "stamp": "a1b2c3d4e5f6", "records": [ /* ... */ ], "count": 3 }
# Crypto-erase a subject's records (irreversible)
curl -X POST https://api.cookiemunch.net/v1/sites/acme-com-9f3k/erase-consent \
  -H "Authorization: Bearer fck_…" \
  -H "User-Agent: Mozilla/5.0" \
  -H "Content-Type: application/json" \
  -d '{ "stamp": "a1b2c3d4e5f6" }'
{ "erased": 3, "encryptionEnabled": true }

erased is the number of records affected. When encryptionEnabled is true, erasure destroys the record's data-encryption key (the hash chain still verifies afterward, since it's computed over ciphertext); when false, this deployment has no consent encryption configured, and "erasure" is best-effort at the storage layer. Both endpoints 400 if stamp is missing.

const { records, count } = await fc.consent.exportSubject('acme-com-9f3k', 'a1b2c3d4e5f6');
const { erased } = await fc.consent.eraseSubject('acme-com-9f3k', 'a1b2c3d4e5f6');

GET /v1/subjects/:subjectId/consent

Cross-surface aggregation: a subject's consent decisions across all of the org's sites, correlated by an app-supplied stable subjectId (set at ingest time via ConsentIngestInput.subjectId — see fc.consent.ingest() — so web, mobile, and desktop surfaces for the same end user resolve to one history). Requires consent:read. Only sites owned by the key's org are scanned.

curl https://api.cookiemunch.net/v1/subjects/user-4471/consent \
  -H "Authorization: Bearer fck_…" \
  -H "User-Agent: Mozilla/5.0"
{
  "subjectId": "user-4471",
  "records": [ /* ConsentLogRow[], flattened across sites */ ],
  "count": 5,
  "sites": ["acme-com-9f3k", "acme-ios-2b7c"]
}

sites lists only the cbids that actually had at least one matching record.

Errors

404 for an unknown-or-foreign cbid; 403 insufficient_scope when the key lacks the required consent:read/consent:write (or receipt:read); 403 with { "error": "domain not verified" } for export/receipt on an unverified site; 501 when receipts aren't configured. See conventions & errors for the full status code table.

Next: sites for domain verification (required by export and receipt), or back to site config.

Was this page helpful?