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, so copy it there and store it server-side. Five active keys per account; the sixth is a 400.
# A key from Settings > API keys. It is shown once, when you create it. # Run this line on its own, in bash or zsh, then paste the key and press # Enter: read -s keeps it off the screen and out of your shell history, # and takes the next line it is given as the key. In CI, set CV_KEY in # the environment instead. printf 'CanaryVaults API key: ' && read -rs CV_KEY && export CV_KEY && echo
# every product endpoint accepts the key header
curl -sS -X POST https://api.canaryvaults.com/canaryshield/inspect \
-H "X-API-Key: $CV_KEY" -H "Content-Type: application/json" \
--data-binary @- <<'JSON'
{"user_id": "<your-user-uuid>", "user_message": "hello"}
JSON<your-user-uuid> is your user ID. The same Settings card shows it, at the top, and every call that names an account sends it as user_id (or in the path); a call that names any other account is refused with a 403. The CanaryRAG, CanaryAudit and CanaryHoneypot consoles end with their calls written out with your ID in them.
The same card lists your active keys, by name, the first characters of each key, and when it was created and last used, and any of them can be revoked there, including one you no longer hold. 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, GET /api/auth/keys, DELETE /api/auth/keys/{key_id} and POST /api/auth/revoke-key (which takes the raw key), and all of them authenticate with a session token 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 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 ["*"], which grants 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.
path prefix read scope write scope /api/admin admin:* admin:* /api/audit profile:read profile:write /api/payments payments:read payments:write /api/privacy privacy:read privacy:write /api/threat-events threats:read threats:read /api/threat-log threats:read threats:read /api/canaries canaries:read canaries:read /api/community community:read community:read /api/breaches breaches:read breaches:write /api/dashboard dashboard:read dashboard:read /api/referral referral:read referral:write /api/settings settings:read settings:write /api/workflow-runs threats:read threats:read /api/weekly threats:read threats:read /api/users profile:read profile:write /api/emails profile:read profile:write /api/onboard profile:write profile:write /canaryagent canaryagent:read canaryagent:write /canaryaudit canaryaudit:read canaryaudit:write /canaryshield canaryshield:read canaryshield:write /canaryhoneypot canaryhoneypot:read canaryhoneypot:write /canaryrag canaryrag:read canaryrag:write
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:
{ "detail": "API key does not have required scope." }No scope, * included, lets a key delete evidence. A CanaryRAG document delete with force=true takes the document's leak records with it, and a CanaryHoneypot trap delete with force=true takes the trap's recorded hits. Both need you signed in to the console; sent with an API key, either is refused with a 403. A key is the credential most likely to leak along with what it guards, and whoever holds one must not be able to delete the record of what they did with it. A key can still delete a document or a trap that holds no evidence.
Evidence & verification
The evidence log is the spine of the platform. Each record is sealed with a SHA-256 content hash at write time. Re-verify anytime: an edited record fails the hash comparison, and the response shows both hashes so the mismatch is visible.
# export the full sealed log (JSON on all plans; legal PDF export on Pro and Enterprise)
curl "https://api.canaryvaults.com/canaryaudit/export/<your-user-uuid>" \
-H "X-API-Key: $CV_KEY" -o evidence.json
# re-verify one record by id
curl -X POST https://api.canaryvaults.com/canaryaudit/verify/<log-id> \
-H "X-API-Key: $CV_KEY"
# → { "verified": true, "tampered": false, "match": true,
# "stored_hash": "…", "recomputed_hash": "…", "sealed": true, … }Hand a customer the content_hash and they can verify it independently at GET /api/verify/{hash} or on the /verify page, with no account. The record id is the key to the owner path above and is useless to anyone else. 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).
{
"detail": {
"error": "plan_limit_exceeded",
"metric": "shield_inspections_per_month",
"plan": "free",
"plan_display_name": "Free",
"limit": 10,
"message": "Your Free plan includes 10 CanaryShield inspections per month and you have used them all. The quota resets at the start of next month, or upgrade at /pricing.",
"upgrade_url": "/pricing",
"used": 10,
"resets_at": "2026-09-01"
}
}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: a 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.
401 missing or invalid API key / session token 402 plan limit reached (see the structured 402 body) 403 key or token does not own the requested resource 404 unknown record, honeypot, or endpoint 422 malformed body (message names the field) 429 rate limited; slow down and retry 5xx transient; retry with backoff
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.
See also