Authentication & scopes
- Written for
- + Written for
- Deprecated
- + Deprecated
- Applies to
- + Applies to
Authentication & scopes
Every request to the Developer API (https://api.cookiemunch.net/v1/*) is authenticated with an API key. There is no orgId parameter anywhere on this surface — the org is derived from the key itself (tenancy.resolveApiKeyAuth), so a key can never be pointed at the wrong org, accidentally or otherwise.
Key format
Keys are generated as fck_ followed by 48 hex characters (24 random bytes):
fck_8f2a91c0b3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2Only a SHA-256 hash of the key is stored server-side (hashApiKey) — the raw key is never persisted and is shown to you exactly once, at creation. The prefix (the first 8 characters, e.g. fck_8f2a91c0) is stored in the clear as display metadata so you can tell keys apart in a list without ever seeing the secret again.
Getting a key
Keys are issued from Settings → API keys in the dashboard (POST /api/v1/keys, session-authenticated, requires the manage_members action — admin or owner role — and accepts an optional name, expiresInDays, and scopes). That's the only way to mint a scoped key.
There's also a self-serve bootstrap route on the Developer API itself, POST /v1/keys, authenticated by an existing key rather than a session:
curl -X POST https://api.cookiemunch.net/v1/keys \
-H "Authorization: Bearer fck_existingkey…"{ "key": "fck_8f2a91c0b3d4e5f6...", "prefix": "fck_8f2a91c0" }This route is admin-only (see below — it lives under /v1/keys) and, notably, does not accept a scopes field: every key it issues is an unscoped (legacy full-access) key, regardless of the scopes on the key that called it. If you want a narrowly-scoped key, you have to create it from the dashboard's POST /api/v1/keys, not from /v1.
Sending the key
Two equivalent forms are accepted on every request — pick whichever your HTTP client makes easiest:
# Bearer token
curl https://api.cookiemunch.net/v1/me -H "Authorization: Bearer fck_…"
# ...or the X-API-Key header (checked first if both are present)
curl https://api.cookiemunch.net/v1/me -H "X-API-Key: fck_…"{ "orgId": "org_9k2m", "plan": "pro", "keyPrefix": "fck_8f2a91c0" }GET /v1/me needs no scope and is the cheapest way to confirm a key works and see which org it resolves to before wiring up real calls.
Scopes
A key is either unscoped (scopes: null — every legacy key, and every key minted via POST /v1/keys) or scoped to an explicit allow-list of resource:action strings. An unscoped key has full access to everything below; a scoped key can only touch what its list grants. The complete, current catalog (API_SCOPES in packages/saas/src/tenancy.ts) is exactly these 11 values — there is no wildcard and no *:*:
Scope | Grants |
|---|---|
| List/get sites, read a site's config, cookies feed, scan status, A/B results, install snippet; list brand kits. |
| Create/delete a site, verify a domain, brand extraction, save config, run v2 flow ops, toggle ad-personalization elements, trigger a scan; create/delete brand kits. |
| Consent log, stats, CSV export, and chain-verification for a site; a subject's records via |
| Crypto-erase a subject ( |
| List DSARs. |
| Create a DSAR; advance a DSAR's status. |
| List vendors (with computed risk scores). |
| Add a vendor. |
| List RoPA (Records of Processing Activities) entries. |
| Add a RoPA entry. |
| Fetch one subject's signed consent receipt by stamp ( |
Issuing a scoped key from the dashboard API:
curl -X POST https://api.cookiemunch.net/api/v1/keys \
-H "Authorization: Bearer <session token>" \
-H "Content-Type: application/json" \
-d '{ "orgId": "org_9k2m", "name": "nightly-export-job", "scopes": ["consent:read"] }'A request whose key lacks the required scope gets a 403:
{ "error": "insufficient_scope", "message": "missing required scope: consent:write" }A gap worth knowing about: /v1/banners has no grantable scope
The account-level banner library (GET/POST /v1/banners, etc.) is scope-checked like everything else — dev-api-auth.ts's resource→scope map requires banners:read (GET) or banners:write (write) to reach it. But banners:read/banners:write are not members of API_SCOPES (see the catalog above — there is no banners:* entry), and areApiScopes() rejects any attempt to save a scope that isn't in that list. The practical effect: no scoped key can ever be issued that satisfies the banner-library check — the dashboard will refuse to save ["banners:read"] on a key, so /v1/banners/* is reachable only with an unscoped (legacy full-access) key today. If you're building an integration that only needs the banner library, be aware you currently have to hold a full-access key for it; treat any docs or key-creation UI that imply a working banners:* scope as aspirational until the scope is added to API_SCOPES.
Admin-only surfaces
Regardless of scope, three resources are only reachable with an unscoped key — there is no scope that grants them, by design, because they're account-administration operations (inviting members, minting other API keys, subscribing webhooks), not data access:
{ "error": "insufficient_scope", "message": "this key may not use admin endpoints; manage them in the dashboard" }This applies to every route under /v1/members, /v1/keys, and /v1/webhooks. GET /v1/me and GET /v1/usage are the opposite case — they require no scope and work with any key, scoped or not.
Error shapes
Status | Body | When |
|---|---|---|
|
| No |
|
| The key doesn't hash-match any stored key, or it's past its |
|
| A scoped key hit a resource it isn't scoped for, or an admin-only surface. |
CORS and the one public route
/v1/* responses always carry permissive CORS headers by default (Access-Control-Allow-Origin: *, since these are server-to-server credentials, not cookie-based — there's nothing to leak cross-origin), unless the deployment sets CORS_ALLOW_ORIGINS, in which case only listed origins are echoed back. The one unauthenticated route on this surface is GET /v1/openapi.json — see OpenAPI.
Next: Conventions & errors for the shared request/response shape, or Sites for the first real resource.