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.

bash
# 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
bash
# 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.

text
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:

json
{ "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.

bash
# 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).

json
{
  "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.

text
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.