Authentication
Every request carries a workspace API key. There is no OAuth client-credentials flow and no username/password endpoint.
Headers
Either header works. Prefer the bearer form.
Authorization: Bearer zfk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
# or
x-api-key: zfk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx-api-key is deliberately absent from our CORS allowlist, so browser calls fail preflight. That is not a bug to work around: a key shipped to a browser is a leaked key.Key format
Keys start with zfk_ followed by 32 bytes of CSPRNG output in base64url. Only a SHA-256 hash is stored on our side, so we cannot show you the secret again — the value appears exactly once, at creation.
Failure responses
Every rejected credential returns the same body. Missing, malformed, unknown, revoked and expired keys are byte-identical apart from the request id.
{
"error": "UNAUTHORIZED",
"message": "Invalid or missing credentials",
"request_id": "req_01J..."
}This is deliberate. Telling a caller which of those five states applies would let someone enumerate valid keys and detect the exact moment one is revoked. The real reason is recorded in the workspace audit log, where an owner can read it.
What each status means
| Status | Meaning | What to do |
|---|---|---|
| 401 | The credential is absent, malformed, unknown, revoked or expired. | Check the key; create a new one if it was revoked. |
403 FORBIDDEN | The key is valid but this capability is never available to keys. | Nothing to retry. Personal-account and internal endpoints are permanently closed to keys. |
403 INSUFFICIENT_SCOPE | The key is valid but this scope was not granted — or the creator's role no longer has it. | Grant the scope, or have an owner create the key. |
| 429 | Rate limit. | Honour Retry-After. |
The authority rule
A key's effective permissions are recomputed on every request:
effective(key) = scopes(key)
∩ capabilities(role of the member who created the key)
∩ scopes that keys may ever holdSo demoting a team member immediately narrows every key they created, and no key can hold a capability its creator never had.
Rotation
Create the new key, deploy it, then revoke the old one. Revocation is immediate and permanent — the row is kept, never deleted, so the record of what that key did survives it.