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.0",
"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 — 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 | Cookie fires in a state where its category is disabled | critical/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 |
Severity ladder is critical > high > medium > low. Receivers parsing severity should treat unknown values as high — 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