Conventions & errors
- Written for
- + Written for
- Deprecated
- + Deprecated
- Applies to
- + Applies to
Conventions & errors
Base URL and versioning
https://api.cookiemunch.net/v1A self-hosted deployment uses its own domain instead — the /v1 prefix is unchanged. The SDK takes the host as baseUrl and appends /v1 for you.
Everything under /v1 is additive-stable: new optional fields and new endpoints can appear without a version bump. A breaking change ships under a new prefix (/v2) rather than changing what /v1 means out from under existing integrations. There is currently no /v2 — the whole Developer API is /v1.
Requests
JSON in, JSON out. Send
Content-Type: application/jsonwith a JSON body onPOST/PUTrequests that take one (a handful, likeDELETE, take none).Path parameters (
:cbid,:id) are always resolved against the key's own org first. Acbidthat belongs to a different org behaves exactly like one that doesn't exist — a plain404, never a403. This is intentional: the API never confirms that another org's resource even exists.OPTIONSis handled generically for CORS preflight on every route and returns204with no auth required.
curl -X PUT https://api.cookiemunch.net/v1/sites/acme-com-9f3k/config \
-H "Authorization: Bearer fck_…" \
-H "Content-Type: application/json" \
-d '{ "blocking": { "mode": "auto" } }'Pagination
There is no pagination on this API. Every list endpoint — GET /v1/sites, GET /v1/dsar, GET /v1/vendors, GET /v1/ropa, GET /v1/members, GET /v1/webhooks, GET /v1/keys, GET /v1/preferences, GET /v1/brand-kits, GET /v1/banners — returns the entire collection in one response body. There is no limit/offset/cursor parameter and no Link header on any of them.
The consent log is the exception and is cursor-paginated. It takes from/to epoch-ms parameters to select a time range, a limit (capped at 1000 per page), and a before cursor (a receivedAt epoch-ms). Each page returns the newest limit records older than the cursor; when more remain, the response carries an X-Next-Cursor header and a Link: <…?before=…>; rel="next" header — follow them to walk the entire history past the 1000/page cap. See Consent log, stats & export.
If you're integrating against an org with a large number of sites, DSARs, or vendors, plan for a single large JSON array in the response rather than a loop over pages.
Idempotency
There is no idempotency-key mechanism. No endpoint reads an Idempotency-Key header or deduplicates by a client-supplied token. A POST that creates a resource (a site, a DSAR, a webhook, an API key) will create a second one if you retry it after a timeout without knowing whether the first attempt landed — check with a GET before retrying a create you're unsure about, rather than retrying blindly. PUT endpoints (like site config) are naturally idempotent because they replace state wholesale rather than append to it.
Rate limiting
A global, per-client-IP token-bucket limiter sits in front of every route on the server, /v1 included — default 240 requests burst capacity, refilling at 40/sec (RATE_CAPACITY / RATE_REFILL_PER_SEC env vars on self-host). It's keyed by IP address, not by API key, so it's a blunt, infrastructure-level backstop rather than a per-key quota. Exceeding it returns:
Every non-preflight response (both 2xx and 429) carries X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset (seconds until a token frees up) so you can self-throttle before hitting the wall. A 429 also includes Retry-After:
HTTP/1.1 429 Too Many Requests
Retry-After: 1
X-RateLimit-Limit: 240
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1{ "error": "rate limit exceeded", "code": "rate_limited" }There's a second, much stricter limiter (~5 requests/minute/IP) but it only applies to the dashboard's /auth/* login/register/SSO routes, not to /v1. CORS preflight (OPTIONS) requests are never counted against either limiter. Beyond this IP-level backstop, there is no separate per-key or per-plan rate limit on the Developer API today.
Responses & status codes
Status | Meaning |
|---|---|
| Success (read, or a write that returns the updated resource). |
| Created (site, DSAR, vendor, RoPA entry, webhook, banner, brand kit, API key). |
| Accepted — an async job (a cookie scan) was started; poll for status. |
| Success, no body (delete, OPTIONS preflight, brand-kit delete, webhook delete). |
| Malformed request — a missing or invalid field. Body includes |
| Missing or invalid API key. |
| Authenticated, but forbidden — insufficient scope, an admin-only surface, or a plan/verification gate (e.g. exporting an unverified domain's consent log). |
| Not found, or it belongs to another org — indistinguishable by design (see above). |
| Conflict — a |
| Rate limited (see above). |
| The endpoint exists in this API version but isn't wired up on this deployment (e.g. self-host without cookie scanning or usage reporting configured). |
Some routes also return 200 { "ok": false, "issues": [...] } for a domain-level validation failure (e.g. an invalid v2 flow config) rather than a 4xx — that's deliberate, so an automation (or an AI agent via the MCP server) can read the issues and retry with a fix, reserving 4xx for auth/not-found/ malformed-request classes of error. See Sites for where this shows up.
Every error body carries a human error string and a stable machine code you can branch on (never parse the prose):
{ "error": "site not found", "code": "not_found" }Codes track the status — bad_request (400), unauthorized (401), forbidden (403), not_found (404), conflict (409), rate_limited (429), not_implemented (501), internal (5xx), and so on. Scope failures carry a more specific code plus a human message (see Authentication & scopes):
{ "error": "insufficient_scope", "message": "missing required scope: consent:write", "code": "insufficient_scope" }Using the SDK, every non-2xx response is thrown as a CookieMunchApiError with .status, .message, and an optional .code:
import { createCookieMunch, CookieMunchApiError } from '@cookiemunch/sdk';
const fc = createCookieMunch({ apiKey: process.env.FC_KEY, baseUrl: 'https://api.cookiemunch.net' });
try {
await fc.sites.get('unknown-cbid');
} catch (e) {
if (e instanceof CookieMunchApiError && e.status === 404) {
console.log('no such site (or not yours)');
}
}Identity & usage
Two meta endpoints work with any key, scoped or not — useful for health checks and usage dashboards without needing a broad grant.
curl https://api.cookiemunch.net/v1/me -H "Authorization: Bearer fck_…"
# { "orgId": "org_9k2m", "plan": "pro", "keyPrefix": "fck_8f2a91c0" }
curl https://api.cookiemunch.net/v1/usage -H "Authorization: Bearer fck_…"
# { "domains": 12, "seats": 4, "monthlyEvents": 812304 }GET /v1/usage returns 501 on a deployment that hasn't wired up usage reporting — a minimal self-host is not required to implement it.
Next: Sites for the first real resource, or OpenAPI to generate a client in another language.