Authentication
Every request to the Fibric API authenticates with a secret API key sent as a Bearer token. The key does three jobs at once: it proves who you are, it names the one tenant you can touch, and it carries the scopes that decide what you can do there. This page covers key anatomy, live and test modes, workspace scoping, least-privilege roles, and rotation. Managing keys over the API itself is documented in the API keys reference.
Shared conventions, including the base URL, pagination, the Idempotency-Key header, and the error envelope, are defined in the API overview. Error codes are catalogued in Errors.
The Authorization header
Send the key in the Authorization header on every request. There is no cookie auth, no query-string token, and no unauthenticated endpoint in the API.
curl https://api.fibric.io/v1/operators \
-H "Authorization: Bearer sk_live_3f9c2a7b8e1d4f60a2c9"
Requests without the header, or with a token the server does not recognize, fail with 401; see 401 codes. A valid key calling a route outside its scopes fails with 403 insufficient_scope.
Key anatomy
Keys are opaque strings with a mode prefix. Never parse anything beyond the prefix; length and alphabet may change within a major version.
| Prefix | Mode | Behavior |
|---|---|---|
sk_live_ | Live | Reads and writes the tenant's real data. Side effects reach real connected systems, gated by governance as always. |
sk_test_ | Test | Operates on a sandboxed copy of the tenant: events, plans, and receipts are real objects in an isolated test partition, and connector side effects run against each connector's test transport instead of the live system. Test data never mixes with live data. |
Live and test are parallel worlds with the same API surface. An id minted in test mode does not resolve with a live key, and the reverse; the mismatch reads as 404 not_found, the same as any cross-boundary id.
The full secret is returned exactly once, at creation. The API and the console store only a hash and a display hint such as sk_live_…a2c9. Keys never appear in receipts, error messages, or logs. If a secret is lost, revoke the key and mint a new one; there is no recovery.
Tenant and workspace scoping
A key belongs to exactly one tenant. The server derives tenant_id and reseller_id from the key on every request and stamps them onto every object it writes. Nothing in a request body can widen this: a body that carries a mismatched tenant_id or reseller_id is rejected with 403 tenant_mismatch, and ids belonging to other tenants read as 404 not_found. The isolation model is described in Tenancy & isolation and the Tenants API.
Within a tenant, a key may optionally be pinned to one workspace. A workspace-pinned key behaves as if the tenant contained only that workspace:
- Objects it creates are stamped with its
workspace_id; a conflictingworkspace_idin the body is rejected with403 tenant_mismatch. - List endpoints return only that workspace's objects; other workspaces' ids read as
404 not_found. - An unpinned key (
workspace_id: null) sees the whole tenant.
Scopes
Scopes are granted per endpoint group and split read from write. Each endpoint's route bar names the scope it requires. The full grant is the union of the scopes on the key; there is no implicit escalation, and receipts has no write scope at all because the ledger is append-only and only the kernel appends.
| Scope | Grants |
|---|---|
events:read / events:write | List and retrieve events; ingest envelopes. |
operators:read / operators:write | List and retrieve operators; create, update, pause, resume. |
connectors:read / connectors:write | List and retrieve connectors; install, test, uninstall. |
plans:read / plans:approve | List and retrieve plans; approve or veto proposed plans. |
actions:read / actions:write | List actions; undo an applied action. |
receipts:read | List, retrieve, and export receipts. |
guardrails:read / guardrails:write | Read and manage guardrail policies; run dry-run evaluations. See the Guardrails API. |
webhooks:read / webhooks:write | Read and manage webhook endpoints and deliveries. See the Webhooks API. |
search:read | Query the search index across events, entities, and receipts. See the Search API. |
tenants:read / tenants:write | Read tenant and workspace settings; manage members. See the Tenants API. |
keys:read / keys:write | List keys; create and revoke keys. See the API keys reference. |
Least-privilege key roles
Rather than assembling scope lists by hand, create keys from a role. A role is a named preset that expands to a fixed scope set at creation time; the key stores the expanded scopes, so a later change to a role's definition never silently changes an existing key.
| Role | Expands to | Use for |
|---|---|---|
read_only | Every :read scope | Dashboards, monitoring, audit tooling. Cannot cause any side effect. |
ingest | events:write | Webhook receivers and gateways that push envelopes in. Cannot read anything back. |
operate | All :read plus plans:approve, actions:write | Human-in-the-loop tooling that approves, vetoes, and undoes. Cannot change operators, connectors, or guardrails. |
admin | Every scope | Provisioning and configuration automation. The only role that can manage keys, tenants, guardrails, and webhooks. |
Grant the smallest role that does the job, one key per workload. A key used by three services is three incidents wide; three ingest keys are each one revocation wide. The API keys page collects the operational practices.
Scopes bound what an API caller may request. Guardrails bound what the executor will run, regardless of who asked. An admin key that approves a plan still cannot make a BLOCK verdict execute; the trust policy fails closed beneath every scope. See Governance & trust.
Key rotation
Rotation is overlap, cut over, revoke. Because a tenant can hold multiple active keys with identical scopes, rotation needs no downtime:
- Create a new key with the same role as the old one (create a key).
- Deploy the new secret to every workload that used the old one.
- Watch the old key's
last_used_atuntil it stops advancing (list keys). - Revoke the old key (revoke a key). Revocation is immediate: the next request with the old key fails with
401 key_revoked.
# 1. mint the replacement with the same role
curl -s -X POST https://api.fibric.io/v1/keys \
-H "Authorization: Bearer $FIBRIC_ADMIN_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: rotate-ingest-2026-07" \
-d '{"name": "ingest-gateway-2026-07", "role": "ingest"}' | jq -r '.secret'
# 2. deploy, then confirm the old key has gone quiet
curl -s https://api.fibric.io/v1/keys/key_4e12ab \
-H "Authorization: Bearer $FIBRIC_ADMIN_KEY" | jq '.last_used_at'
# 3. revoke the old key
curl -s -X DELETE https://api.fibric.io/v1/keys/key_4e12ab \
-H "Authorization: Bearer $FIBRIC_ADMIN_KEY"
Rotate on a schedule that matches the key's blast radius, and rotate immediately on any suspicion of exposure. Key creation and revocation are themselves written to the receipt ledger, so a rotation leaves an audit trail.
Handling secrets in clients
- Load keys from the environment or a secret manager, never from source. The examples in these docs use
$FIBRIC_KEYfor this reason. - Use
sk_test_keys in CI and development. A test key in a leaked log is an inconvenience; a live key is an incident. - Do not send keys from browsers or mobile apps. The API is server-to-server; front ends should call your backend, which holds the key.
- Strip
Authorizationfrom any request logging middleware. The API never echoes the key back, so your logs are the only place it can leak.
Authentication errors
Every authentication failure uses the standard error envelope. The codes you will branch on:
| Status | Code | When |
|---|---|---|
401 | unauthenticated | The Authorization header is missing or is not a Bearer token. |
401 | key_invalid | The token is not a key the server recognizes. Check for whitespace, truncation, and live-versus-test mode. |
401 | key_revoked | The key existed but has been revoked. Mint a new key and redeploy. |
403 | insufficient_scope | The key is valid but lacks the scope the route requires. |
403 | tenant_mismatch | The body carried a tenant_id, reseller_id, or workspace_id that does not match the key. |
None of these are retryable unchanged; fix the credential or the request first. The full retryability table is in Errors.