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 (default 10, max 50). Caps total pages crawled.
  • maxDepth — optional (default 3, max 5). 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 and callbackUrl is provided, CookieBoss generates one. Min 16 chars.
  • webhookFormat — optional, "summary" (default) or "full". With "summary" you get the lightweight scan.completed event and fetch the result via GET. With "full" you receive scan.completed.full with 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 OK with full payload — scan completed. See Result Shape.
  • 200 OK with { "status": "failed", "errorMessage": "..." } — scan failed.
  • 404 Not Found — unknown scanId, 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_key is sha256_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_id is 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).

TypeDescriptionSeverity
pre_consentNon-necessary cookie set before any consentcritical/high/medium
category_leakCookie fires in a state where its category is disabledcritical/high
uncategorizedCookie observed but not classifiedlow
miscategorizedCMP declaration differs from CookieBoss classificationmedium
third_party_request_before_consentThird-party request before consent grantedhigh
excessive_lifetimeCookie lifetime exceeds 12 months (Datatilsynet guidance)medium
fingerprinting_without_consentFingerprinting API used without consenthigh

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

PlanScans/hour per keymaxPages cap
Pro1025
Business1050
EnterpriseOn request50

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 key
  • 403 forbidden — API key’s plan doesn’t include v2 scanning
  • 404 not_found — unknown scanId or scan belongs to a different customer
  • 429 rate_limit_exceeded — hourly quota exceeded
  • 500 internal_error — transient failure; safe to retry with a new scanId