Reference
API reference
Authentication
Product API requests authenticate with a workspace API key sent in the X-API-Key header. Keys look like cv_<id>_<secret> and are minted in the app, at Settings → API keys → Generate API key, on every plan including Free. The full key is shown once at creation and is not recoverable afterwards — copy it there and store it server-side. Five active keys per account; the sixth is a 400.
Revoke on the same card, while the key is still on screen: revocation takes the raw key, not a key id, and there is no list-keys endpoint to recover one from — a key you navigated away from cannot be revoked here. A revoked key stops authenticating immediately; existing sealed records stay valid, and any calls made with the old key remain in the audit trail.
The routes behind that card are POST /api/auth/generate-key and POST /api/auth/revoke-key, and both authenticate with a Supabase session JWT rather than with an API key. That token belongs to the web app — held in memory, with an HttpOnly cookie as its durable half — so it is not something you can export into a terminal. Drive these two from the UI; every other endpoint on this page takes X-API-Key.
Key scopes
Every API key carries a scope list. POST /api/auth/generate-key accepts an optional scopes array (at most 20 entries); omit it and the key is created with ["*"] — access to everything. Scope strings are lower-cased and de-duplicated, entries outside the allowed character set are dropped, and if that leaves nothing the key falls back to ["*"]. The Settings card sends no scopes today, so a key minted there is an ["*"] key; narrowing one needs a scope picker in the UI that does not exist yet.
Scopes are checked on requests that carry X-API-Key. The required scope comes from the path prefix (first match wins, in the order below) and from the HTTP method — POST, PUT, PATCH and DELETE count as writes, every other method as a read.
Two wildcards are accepted: * matches every scope, and <namespace>:* — the part before the first colon, e.g. canaryshield:* — matches both the read and the write scope of that namespace. A path prefix that is not in the table is not scope-gated by this layer; the route still authenticates on its own. A key without the required scope is rejected with 403:
Evidence & verification
The evidence log is the spine of the platform. Each record is sealed with a SHA-256 content hash the moment it is written. Re-verify anytime — an edited record fails the hash comparison, and the response shows both hashes so the mismatch is visible.
Hand a customer a record id and its hash and they can verify independently — the point is history you can demonstrate, not just assert.
Plan limits, 402s, 429s and the daily backstop
When you exceed a per-month or per-day plan limit, the API returns a structured 402 — never a silent failure. Existing traps keep alerting; only new actions are limited. The body names the metric, your plan, and the limit.upgrade_url is a site-relative path, and per-month quotas also carry used and resets_at (the UTC date the meter rolls over).
A metered surface is bounded at three horizons, and only two of them answer 402. The per-month quota and the shorter per-account daily allowance both return the body above; the daily one differs only in carrying resets_at for the next UTC day rather than the first of next month. The per-minute burst limit answers 429 instead, with burst_limit_exceeded as its error, a retry_after_seconds field and a matching Retry-After header. That it is not a 402 is deliberate: you are not out of quota, you are going too fast, and because the burst check runs before either meter a request it turns away has spent nothing and needs no refund.
The daily allowance is also the one meter here that fails closed. Everything else on this page waves a request through when metering itself is unavailable, on the grounds that a metering outage should not take the product down; the daily backstop exists for exactly the case where the others have stopped enforcing, so it refuses instead — 503, with daily_quota_unavailable as its error. It is a 503 rather than a 402 because you are not over any limit: your usage could not be checked, and reporting that as a quota refusal would be untrue. Retry shortly.
Errors & rate limits
Errors use standard HTTP status codes with a JSON detail body carrying a human-readable message (structured bodies, like the 402 above, name a stable error code). Rate-limited endpoints return 429.
Every response includes a request id in the X-Request-ID header (send your own X-Request-ID to correlate) — include it when contacting support.