Skip to content

API Keys ​

Tenant-facing API key management. Mint named keys for each integration (frontend, ERP, mobile app, sandbox test rig, etc.), list them, and revoke leaked or unused ones.

GET    /v1/keys
POST   /v1/keys
DELETE /v1/keys/:id
GET    /v1/keys/:id/usage

Authentication ​

Authorization: Bearer <api-key> — any active key for the tenant with the keys:manage scope. Every endpoint on this page manages keys themselves, so a narrower key (e.g. a documents:read-only key) cannot list, create, revoke, or view usage for keys. Holding keys:manage alone still isn't enough to escalate privilege — see Mint a new key's privilege containment rule below. See Scopes below.


Scopes ​

Every key carries a scopes array. A request is only allowed through if the key's scopes include the scope the target route requires — see Insufficient Scope for the error shape when it doesn't.

ScopeCovers
documents:writeAll document mutations (POST /v1/documents, /send, /rebuild, /email-retry, etc.) and GET /:accessKey/authorize (it triggers a live SRI call and can send an email, so it's treated like a write). Also POST /v1/tenants/retry-failed-documents.
documents:readEvery other GET under /v1/documents (list, get, RIDE, XML, events, credit notes, SRI responses, stats).
issuers:readEvery GET under /v1/issuers (list, get, document types, sequentials).
issuers:writeEvery POST/PATCH/DELETE under /v1/issuers (branch creation, updates, logo, certificate renewal, document types, sequentials).
keys:manageThis entire /v1/keys router.
billing:manageGET /v1/subscriptions/me, GET /v1/payments/:id/proofs, and GET /v1/payments/:id/proofs/:proofId — the only three /v1/subscriptions//v1/payments endpoints a key can call directly. Every other endpoint in those two routers (starting a subscription, changing tier/seats, cancelling, submitting or deleting proof, card payments) can only be called by the Comprobify web app itself — see Your subscription & billing.
webhooks:manage/v1/webhooks in full.
tenant:managePATCH /v1/tenants/language. (It also governs accepting the legal agreements, which is only done from the web app.)
tenant:promoteGoing to production — split out from tenant:manage since it mints/revokes every one of the tenant's keys and flips sandbox→production irreversibly. That action is only done from the web app, so a key of your own with this scope cannot perform it on its own.

A tenant's very first key (minted automatically at registration) always gets all nine — full access, identical to how every key behaved before scopes existed. Minting an additional key via POST /v1/keys is different: omitting scopes there does not default to full access — it clones whatever scopes the key making that call already has (see Mint a new key below for the full rule). You can still always create a key without passing scopes at all; you just get a copy of your own key's scopes rather than a blanket full-access grant. Scoping down further is opt-in: pass an explicit, narrower scopes array. Basic identity reads (GET /v1/tenants/me, /agreements, /events) and notification/catalog endpoints are scope-exempt — any active key can call them regardless of its scopes array.


List keys ​

GET /v1/keys

Returns every active key for the tenant. The plaintext token is never returned — only labels, environments, and ids.

Response ​

json
{
  "ok": true,
  "keys": [
    {
      "id": "00000000-0000-0000-0000-000000000017",
      "label": "frontend-prod",
      "environment": "production",
      "scopes": ["documents:write", "documents:read", "issuers:read", "issuers:write", "keys:manage", "billing:manage", "webhooks:manage", "tenant:manage", "tenant:promote"],
      "active": true,
      "createdAt": "2026-03-01T12:00:00.000Z",
      "revokedAt": null,
      "lastUsedAt": "2026-08-07T14:22:10.000Z",
      "requestCount": 15832
    },
    {
      "id": "00000000-0000-0000-0000-000000000018",
      "label": "dashboard-readonly",
      "environment": "production",
      "scopes": ["documents:read"],
      "active": true,
      "createdAt": "2026-04-12T09:30:00.000Z",
      "revokedAt": null,
      "lastUsedAt": null,
      "requestCount": 0
    }
  ],
  "limit": { "max": 5, "used": 2 }
}

lastUsedAt (nullable, null if the key has never authenticated a request) and requestCount (lifetime counter, not windowed — for time-boxed volume use the structured request logs or an APM tool) update on every request that key successfully authenticates. scopes reflects what that key is currently permitted to do — see Scopes above.

limit.max is how many active keys you can have in total before POST /v1/keys returns 402 API_KEY_LIMIT_REACHED, and limit.used is your current count — it matches your plan's own maximum exactly (see Subscription tiers). On Free/Solo/Lite max is 0: those plans don't sell additional keys via self-service, and the internal keys the web app uses to run your dashboard never count toward this or appear in this listing.

Errors ​

StatusCodeWhen
401UNAUTHORIZEDMissing or invalid API key
403INSUFFICIENT_SCOPEThe key making this request doesn't have the keys:manage scope

Mint a new key ​

POST /v1/keys

Creates a new tenant-scoped key. The plaintext token is shown once in the response and never stored — record it immediately.

Request body ​

json
{
  "label": "dashboard-readonly",
  "environment": "sandbox",
  "scopes": ["documents:read"]
}
FieldTypeRequiredDefaultDescription
labelstringNonullHuman-readable name for the integration (max 100 chars). Highly recommended for observability.
environmentstringNo"sandbox"Either "sandbox" or "production". Production keys can only be minted after the tenant has been promoted to production.
scopesstring[]Noa copy of the requesting key's own scopesNon-empty array, each entry one of the 9 values listed in Scopes above. Omitting it does not default to full access — it clones whatever scopes the key making this call already has.

Privilege containment: every entry in scopes must already be held by the key making this request — you cannot mint a key broader than yourself, even if you hold keys:manage. A full-access key can mint any combination (including another full-access key); a key with only ["keys:manage", "documents:read"] can mint a key with ["documents:read"] but not one with ["documents:write"].

Plan limit: each tier caps how many active keys a tenant can have at once (see Subscription tiers). Revoke an unused key via DELETE /v1/keys/:id to free up a slot, or upgrade your plan.

Response ​

201 Created

json
{
  "ok": true,
  "apiKey": "a3f8c2bd9e10...",
  "scopes": ["documents:read"]
}

The plaintext token (apiKey) is shown once; scopes echoes back what was actually granted (useful to confirm the default was applied when the field was omitted).

Errors ​

StatusCodeWhen
400VALIDATION_FAILEDlabel too long, environment invalid, or scopes is present but not a non-empty array of valid scope strings
401UNAUTHORIZEDMissing or invalid API key
403FORBIDDENTenant email not verified, OR attempting to mint a production key before any issuer has been promoted
403INSUFFICIENT_SCOPEThe key making this request doesn't have the keys:manage scope
403SCOPE_ESCALATION_FORBIDDENscopes includes an entry the requesting key doesn't itself hold — see Privilege containment above
402API_KEY_LIMIT_REACHEDTenant already has the maximum number of active keys their plan allows — see Subscription tiers

Revoke a key ​

DELETE /v1/keys/:id

Marks the key as inactive. The key cannot be used to authenticate any future request.

Path parameters ​

ParameterDescription
idUUID of the key (from GET /v1/keys)

Response ​

200 OK

json
{ "ok": true }

Errors ​

StatusCodeWhen
400BAD_REQUESTAttempting to revoke the same key you are using to make this request — use a different key, or coordinate with admin support
401UNAUTHORIZEDMissing or invalid API key
403INSUFFICIENT_SCOPEThe key making this request doesn't have the keys:manage scope
404NOT_FOUNDKey id does not exist or already revoked, or belongs to a different tenant

Daily key usage ​

GET /v1/keys/:id/usage

Returns a daily series of authenticated requests for that key, with exactly one entry per day in the range — idle days are included with requestCount: 0 rather than omitted, so there are no gaps to backfill before charting the series.

Path parameters ​

ParameterDescription
idUUID of the key (from GET /v1/keys)

Query parameters ​

ParameterTypeRequiredDefaultDescription
daysintegerNo30How many days back to include (1–365), counting today.
http
GET /v1/keys/00000000-0000-0000-0000-000000000501/usage?days=7
Authorization: Bearer <your-api-key>

Response ​

200 OK

json
{
  "ok": true,
  "usage": [
    { "date": "2026-08-05", "requestCount": 0 },
    { "date": "2026-08-06", "requestCount": 128 },
    { "date": "2026-08-07", "requestCount": 342 }
  ]
}

The series is zero-filled — there are always exactly days entries, one per day in the range, even for days the key was never used. The id may belong to an already-revoked key (ownership, not active status, gates access), so a revoked key's history stays queryable.

Errors ​

StatusCodeWhen
400VALIDATION_FAILEDid is not a valid UUID, or days is outside the 1–365 range
401UNAUTHORIZEDMissing or invalid API key
403INSUFFICIENT_SCOPEThe key making this request doesn't have the keys:manage scope
404NOT_FOUNDKey id does not exist or belongs to a different tenant

Key environment + targeted issuer ​

When a key is used on a document request, the resolveIssuer middleware validates that the key's environment matches the targeted issuer's effective environment. The sandbox flag lives on the tenant — resolveIssuer reads tenant.sandbox and rejects any key/issuer mismatch:

Key environmentTenant sandboxResult
sandboxtrueOK
sandboxfalse401 — sandbox key cannot address a production tenant
productiontrue401 — production key cannot address a sandbox tenant
productionfalseOK

This is the only safeguard preventing accidental cross-environment requests; treat the environment as part of the key's identity, not as a separate detail from it.

Comprobify API Documentation — API v1.3.1