Scanning API v2
The v2 scanning API runs a multi-page crawl in every consent state (none / necessary / functional / analytics / marketing / all) and emits a richly structured result: cookies per state, per-page network requests, storage writes, violations, score breakdown, and a diff vs your previous scan.
Use v2 when you need compliance evidence across consent states (e.g. “do analytics cookies actually stop firing when the user rejects analytics?”). For a simpler single-shot scan, use v1.
v2 scans are asynchronous. A full 12-page × 6-state scan typically takes 2–5 minutes. Either poll GET /api/v2/scan/{scanId} or register a webhook.
Authentication
All v2 endpoints require an API key (X-API-Key: cb_live_…). Available on Pro, Business, and Enterprise plans. See Authentication for details.
Default quota: 10 scans per hour per API key. Requests that exceed the quota return 429.
Triggering a Scan
POST /api/v2/scan Start an async multi-consent-state scan
Request body:
{
"domain": "lyse.no",
"consentStates": ["none", "necessary", "functional", "analytics", "marketing", "all"],
"maxPages": 12,
"maxDepth": 3,
"callbackUrl": "https://example.com/webhooks/cookieboss",
"callbackSecret": "your-shared-secret-32-chars-min",
"webhookFormat": "summary"
}Fields:
domain: apex or subdomain to scan. IPs and private/internal hosts are rejected.consentStates: optional, defaults to all six states. Any subset is valid. Accepts both the canonical names above (functional,analytics,marketing) and additive aliases (necessary+preferences,necessary+preferences+analytics,necessary+preferences+analytics+marketing).maxPages: optional (default10, max50). Caps total pages crawled.maxDepth: optional (default3, max5). Caps BFS depth from the homepage.callbackUrl: optional HTTPS URL that receives a signed webhook on completion. Private/internal hosts are rejected.callbackSecret: optional shared secret used to HMAC-sign the webhook payload. If omitted andcallbackUrlis provided, CookieBoss generates one. Min 16 chars.webhookFormat: optional,"summary"(default) or"full". With"summary"you get the lightweightscan.completedevent and fetch the result via GET. With"full"you receivescan.completed.fullwith the entire result inline (snake_case schema, see Full Payload Webhook).
Returns 202 Accepted with the scanId. estimatedDuration is in seconds.
Response
{
"scanId": "scn_01HKQ...",
"status": "queued",
"estimatedDuration": 420
} Fetching Results
GET /api/v2/scan/:scanId Fetch scan status or completed result
Three possible responses:
202 Accepted: scan still running:{ "scanId": "scn_...", "status": "running", "startedAt": "2026-04-18T10:15:00Z" }200 OKwith full payload: scan completed. See Result Shape.200 OKwith{ "status": "failed", "errorMessage": "..." }: scan failed.404 Not Found: unknownscanId, or scan belongs to a different customer.
Payloads are retained for 12 months after completion.
Webhook Delivery
When you pass a callbackUrl, CookieBoss POSTs a signed notification when the scan completes.
POST <callbackUrl>
Content-Type: application/json
X-CookieBoss-Signature: sha256=<hex>
X-CookieBoss-Event: scan.completed
X-CookieBoss-Delivery: <uuid>
{
"event": "scan.completed",
"scan_id": "scn_...",
"domain": "lyse.no",
"status": "completed",
"completed_at": "2026-04-18T10:22:31Z",
"signed_at": "2026-04-18T10:22:31Z",
"partial": false,
"pages_failed": 0,
"score": { "overall": 62, "grade": "D" }
}
Signature verification: Compute HMAC-SHA256 of the raw request body using your callbackSecret, hex-encode, and compare to the value after sha256= in X-CookieBoss-Signature. Use a constant-time comparison.
Replay protection: signed_at is the wall-clock time at which we computed the signature. Reject deliveries with a skew greater than 5 minutes from your server’s clock.
Retry policy: On any non-2xx response or network error, delivery is retried up to 6 times with backoff 30s / 2m / 10m / 1h / 6h / 24h. After exhausting retries, callback_status on the scan is marked failed.
Idempotency: X-CookieBoss-Delivery is a UUID assigned on the first dispatch and reused on every retry of the same payload. Receivers should dedup on this header.
The summary webhook only contains a status snapshot. Call GET /api/v2/scan/{scanId} to fetch the full result, or set webhookFormat: "full" to get it inline (see below).
Full Payload Webhook
Setting webhookFormat: "full" on the scan submission switches the webhook event to scan.completed.full and inlines the entire scan result in the body. Fields are snake_case. Suitable for compliance integrations that want to act on the scan without a follow-up GET.
POST <callbackUrl>
Content-Type: application/json
X-CookieBoss-Signature: sha256=<hex>
X-CookieBoss-Event: scan.completed.full
X-CookieBoss-Delivery: <uuid>
{
"event": "scan.completed.full",
"scan_id": "scn_...",
"site_id": null,
"domain": "lyse.no",
"scanned_at": "2026-04-18T10:22:31Z",
"signed_at": "2026-04-18T10:22:31Z",
"pages_scanned": 12,
"partial": false,
"pages_failed": 0,
"score": {
"overall": { "score": 62, "grade": "D" },
"gdpr": { "score": 58, "grade": "D" },
"ccpa": { "score": 71, "grade": "C" },
"eprivacy":{ "score": 54, "grade": "F" }
},
"score_version": "2.5",
"consent_states": [
{ "state": "none", "cookies_set": 4, "storage_items": 1, "network_requests": 12, "violation_count": 7, "is_baseline": true },
{ "state": "necessary+preferences+analytics+marketing", "cookies_set": 19, "storage_items": 6, "network_requests": 41, "violation_count": 1, "is_baseline": false }
],
"violations": [
{
"violation_key": "9f2a...e1",
"type": "pre_consent_cookie",
"severity": "critical",
"consent_state_when_observed": "none",
"cookie_or_resource_name": "_fbp",
"vendor": "Meta",
"description": "_fbp (Meta) is set before the user gives consent.",
"description_no": "_fbp (Meta) settes før brukeren gir samtykke.",
"suggested_fix": "Block _fbp until the user accepts the marketing consent category."
}
],
"cookies": [
{
"name": "_hjSession_5844",
"domain": ".hotjar.com",
"category": "analytics",
"vendor": "Hotjar",
"purpose": null,
"expiry_days": 30,
"set_before_consent": false,
"is_first_party": false
}
],
"tracking_domains": ["static.hotjar.com", "connect.facebook.net"],
"storage_items": [
{ "type": "localStorage", "key": "hjClosedSurveyInvites", "domain": "lyse.no", "set_before_consent": false }
],
"fingerprints": []
}
Stable identity:
violation_keyissha256_hex(type + ":" + cookie_or_resource_name + ":" + consent_state_when_observed). The same offending asset in the same consent state across two scans always yields the same key, so you can track resolution over time by joining on this column.site_idis null for ad-hoc scans; non-null when CookieBoss has a stable identifier for the domain.- Consent state values use the additive form (
necessary+preferences, …). On the request side, both forms are accepted.
Result Shape
{
"scanId": "scn_01HKQ...",
"status": "completed",
"domain": "lyse.no",
"startedAt": "2026-04-18T10:15:00Z",
"completedAt": "2026-04-18T10:22:31Z",
"durationMs": 451234,
"pagesScanned": ["https://lyse.no/", "https://lyse.no/strom", "..."],
"consentStates": ["none", "necessary", "functional", "analytics", "marketing", "all"],
"perConsentState": [
{
"state": "marketing",
"cookies": [
{
"name": "_hjSession_5844",
"domain": ".lyse.no",
"category": "analytics",
"vendor": "Hotjar",
"expiryDays": 30,
"valueHash": "a1b2c3d4e5f6g7h8",
"sameSite": "Lax",
"httpOnly": false,
"secure": true,
"isFirstParty": true,
"setBeforeConsent": false
}
],
"storage": [
{ "kind": "localStorage", "key": "hjClosedSurveyInvites", "valueHash": null, "page": "https://lyse.no/strom" }
],
"requests": [
{
"url": "https://static.hotjar.com/c/hotjar-12345.js",
"method": "GET",
"resourceType": "script",
"destinationDomain": "static.hotjar.com",
"page": "https://lyse.no/strom",
"isThirdParty": true
}
],
"violations": [
{
"type": "category_leak",
"severity": "high",
"consentedCategory": "marketing",
"actualCategory": "analytics",
"asset": { "kind": "cookie", "name": "_hjSession_5844", "vendor": "Hotjar", "category": "analytics" },
"page": "",
"evidence": "_hjSession_5844 fired in marketing consent state but requires analytics consent"
}
]
}
],
"cookieInventory": [
{
"name": "_hjSession_5844",
"domain": ".lyse.no",
"category": "analytics",
"vendor": "Hotjar",
"expiryDays": 30,
"firstSeenInState": "analytics",
"firstSeenOnPage": "",
"preConsent": false
}
],
"score": {
"overall": 62,
"grade": "D",
"version": "1.0",
"breakdown": [
{ "dimension": "all", "deduction": 15, "description": "3 non-necessary cookie(s) set before consent" },
{ "dimension": "all", "deduction": 10, "description": "7 third-party tracking request(s) before consent" }
]
},
"changes": {
"previousScanId": "scn_01HJX...",
"newCookies": [],
"removedCookies": [],
"newViolations": [],
"resolvedViolations": []
}
}
Cookie values are never stored. valueHash is a 16-char prefix of sha256(value), enough to detect “same cookie, different value” across scans without holding pseudonymous identifiers.
Violation Types
The summary GET response and perConsentState[].violations use the internal types below. The full-payload webhook (scan.completed.full) translates them to the public wire enum (pre_consent_cookie, long_lifetime, third_party_pixel, fingerprinting, missing_disclosure, category_leak, consent_string_invalid, other).
| Type | Description | Severity |
|---|---|---|
pre_consent | Non-necessary cookie set before any consent | critical/high/medium |
category_leak | A cookie, or a tracker request (asset.kind: "request"), fires in a state where its category is disabled | critical/high (low for a cookieless Google ping) |
uncategorized | Cookie observed but not classified | low |
miscategorized | CMP declaration differs from CookieBoss classification | medium |
third_party_request_before_consent | Before consent, or after the visitor chose necessary only: a tracker beacon (high), Google Fonts (medium), or a Google ping that carries consent denied, gcs=G100 (low). The site’s consent tool, CDNs and other assets are not findings | high, medium or low |
excessive_lifetime | Cookie lifetime exceeds 12 months (reported once per cookie) | medium |
fingerprinting_without_consent | Fingerprinting API used without consent | high |
Severity ladder is critical > high > medium > low. Receivers parsing severity should treat unknown values as high, because new severities may be added.
CMP Support
Tier-1 support (tested, reliable): Cookiebot.
Best-effort (CMP detected and consent cookies injected; validate output per site): OneTrust, Cookie Information, Didomi, Usercentrics, Quantcast, Sourcepoint, CookieYes, Complianz, Termly, Iubenda, TrustArc, and 15+ others.
If no CMP is detected on your site, CookieBoss still runs the none and all states, but per-category states may return identical results.
Scoring
The compliance score is a fixed algorithm, currently version 2.0, stamped on every scan as score.version. The algorithm is not silently retuned between releases. A new version is cut if the weights change, and consumers can filter historical scans by version. Older scans retain the version they were computed under.
Each scan returns four scores: an overall and three per-jurisdiction (gdpr, ccpa, eprivacy). The per-jurisdiction scores apply different weights to the same set of violations. For example, ePrivacy penalises any pre-consent storage heavily, while CCPA emphasises sale-of-data signals.
Deductions are returned in score.breakdown with per-jurisdiction attribution.
Example: End-to-end Flow
# 1. Trigger scan
curl -X POST https://api.cookieboss.io/api/v2/scan \
-H "X-API-Key: cb_live_..." \
-H "Content-Type: application/json" \
-d '{
"domain": "lyse.no",
"callbackUrl": "https://example.com/webhook",
"callbackSecret": "my-shared-secret-32-chars-min"
}'
# → { "scanId": "scn_01HKQ...", "status": "queued", "estimatedDuration": 420 }
# 2. Wait for webhook, OR poll:
curl https://api.cookieboss.io/api/v2/scan/scn_01HKQ... \
-H "X-API-Key: cb_live_..."
# 3. When status=completed, the same GET returns the full result payload.
Webhook Operations
Redeliver a Webhook
POST /api/v2/scan/:scanId/redeliver Re-fire the webhook for a completed scan
Use when a receiver missed a delivery (deploy outage, clock skew, etc.). Retries pick up the same X-CookieBoss-Delivery UUID so receivers continue to dedup correctly. The full backoff schedule (6 attempts) starts fresh.
curl -X POST https://api.cookieboss.io/api/v2/scan/scn_01HKQ.../redeliver \
-H "X-API-Key: cb_live_..."
# → 202 { "scanId": "scn_01HKQ...", "status": "pending" } Rotate Secret
POST /api/v2/scan/:scanId/rotate-secret Issue a new HMAC secret for the scan's callback URL
The previous secret remains valid for 7 days so receivers can verify against either during the overlap. Returned newSecret is shown once.
curl -X POST https://api.cookieboss.io/api/v2/scan/scn_01HKQ.../rotate-secret \
-H "X-API-Key: cb_live_..."
# → 200 { "scanId": "...", "newSecret": "...", "previousSecretValidUntil": "..." } Test a Webhook URL
POST /api/v2/webhooks/test Send a fixture scan.completed payload to a callbackUrl
Verify connectivity, signature handling, and routing without running a real scan. The response reports the receiver’s status code.
curl -X POST https://api.cookieboss.io/api/v2/webhooks/test \
-H "X-API-Key: cb_live_..." \
-H "Content-Type: application/json" \
-d '{
"callbackUrl": "https://example.com/webhook",
"callbackSecret": "shared-secret-32-chars-min"
}'
# → 200 { "ok": true, "status": 200, "deliveryId": "..." } Rate Limits & Quotas
| Plan | Scans/hour per key | maxPages cap |
|---|---|---|
| Pro | 10 | 25 |
| Business | 10 | 50 |
| Enterprise | On request | 50 |
Rate limits are tracked per API key in hourly buckets. Exceeding returns 429 with:
{ "error": "rate_limit_exceeded", "limit": 10, "windowStart": "2026-04-18T10:00:00.000Z" }
Error Responses
400 validation_error: request body failed validation (invalid domain, callback URL, etc.)401 unauthorized: missing or invalid API key403 forbidden: API key’s plan doesn’t include v2 scanning404 not_found: unknownscanIdor scan belongs to a different customer429 rate_limit_exceeded: hourly quota exceeded500 internal_error: transient failure; safe to retry with a newscanId
Score versions
score_version changes whenever the rules behind the score change, so scores from different versions should not be compared directly. A stored score keeps its version; scan the site again to get the current one.
| Version | Change |
|---|---|
| 2.5 | Cookieless analytics beacons (Plausible) rate low, like Google pings with consent denied, instead of high. |
| 2.4 | In the analytics-only, marketing-only and functional-only states, a tracker beacon whose category that state did not grant is reported as category_leak (asset.kind: "request"): critical for marketing, high for analytics. A Google ping that is cookieless for the denied category is low. |
| 2.3 | Requests after the visitor chose necessary only are judged like requests before consent. A scan where a state loaded pages but captured no network requests is marked partial. |
| 2.2 | Before consent, only tracker beacons (high), Google Fonts (medium) and Google pings with consent denied (low) count; the site’s consent tool, CDNs and other assets no longer do. Lifetime and unclassified-cookie findings are reported once per cookie instead of once per consent state. |