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"
}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.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 signedscan.completedwebhook. 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.
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: <ulid>
{
"event": "scan.completed",
"scanId": "scn_...",
"domain": "lyse.no",
"status": "completed",
"completedAt": "2026-04-18T10:22:31Z",
"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.
Retry policy: On any non-2xx response or network error, delivery is retried up to 3 times with exponential backoff (5s, 30s, 5min). After exhausting retries, callback_status on the scan is marked failed.
The webhook only contains the summary. Call GET /api/v2/scan/{scanId} to fetch the full result.
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
| Type | Description | Severity |
|---|---|---|
pre_consent | Non-necessary cookie set before any consent | high/medium |
category_leak | Cookie fires in a state where its category is disabled | high |
uncategorized | Cookie observed but not classified | low |
miscategorized | CMP declaration differs from CookieBoss classification | medium |
third_party_request_before_consent | Third-party request before consent granted | high |
excessive_lifetime | Cookie lifetime exceeds 12 months (Datatilsynet guidance) | medium |
fingerprinting_without_consent | Fingerprinting API used without consent | high |
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, versioned as scoreVersion: "1.0" in the response. 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.
Deductions are returned in score.breakdown with per-jurisdiction attribution (gdpr, ccpa, eprivacy, or all).
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.
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