Skip to content
Beta. This surface may change before GA; breaking changes are dated in the changelog.

Handing out keys

A Brain key reaches exactly one Brain with exactly the capabilities you grant. It is what you hand to anything you do not run yourself.

Terminal window
curl -X POST "$API/v1/brains/tenant:acme/keys" \
-H "authorization: Bearer $NICIA_KEY" \
-H "content-type: application/json" \
-d '{ "name": "Acme web app", "capabilities": ["read"] }'
{
"data": {
"key": {
"id": "key_7f3a1c",
"name": "Acme web app",
"capabilities": ["read"],
"status": "active",
"createdAt": "2026-08-14T09:41:02Z"
},
"secret": "nbk_9f2c…"
}
}

secret is shown once. Nicia stores only its hash. Store it now; a lost secret is replaced by rotating, never recovered.

{ "name": "acme dashboard", "capabilities": ["read"] }

Read-only, one tenant. Safe in a server-rendered app. Never ship it to a browser — see End users below.

{ "name": "salesforce sync", "capabilities": ["read", "write"] }

A key says what this writer may reach, never how its writes are admitted. Whether a write lands or stages is the Brain’s mode, and the Brain’s admission policy decides it for every write, whoever sent it. See Review.

Give the key a branch alongside its capabilities:

{ "name": "draft agent", "capabilities": ["read", "write"], "branch": "branch-id" }

The branch must already exist. This key can write records on that branch, compare it with main, and open a Proposal from it. It cannot read or write main, use main’s receipts or events, address another branch, or mint another key. Rotating it keeps the branch scope. Apply the Proposal with a reviewer key under main’s admission policy.

A key scoped to a read-only branch is read-only: mint it with ["read"]. Asking for any other capability, then or later, is a 409.

{
"name": "finance",
"capabilities": ["read", "write", "review", "admin"],
"expiresAt": "2027-01-01T00:00:00Z"
}

Corporate IT provisions a Brain per department with a management key, then hands each department a key that governs their own Brain and nothing else. admin is the review switch — PATCH /v1/brains/{handle}, which turns review on or off — but not the ability to read the Brain’s contents, which is a separate capability on purpose. That separation is only worth anything if it holds for bulk egress too, so admin does not carry export or webhook control either: export is served on read, and webhooks are not on the Brain-key surface today. The department above reads, and so can export, because it also holds read. Creating and deleting a Brain, listing the organization’s Brains, reading a Brain’s schema lineage, and the whole key plane — minting, listing, rotating, and revoking keys — stay with IT’s management key. No Brain key reaches any of them whatever capabilities it holds, so a department cannot mint sub-keys off the key it was given. See Capabilities.

Derive the idempotency key from something stable on your side and a retried provisioning run returns the same key instead of minting a second live secret:

Terminal window
curl -X POST "$API/v1/brains/tenant:acme/keys" \
-H "authorization: Bearer $NICIA_KEY" \
-H "content-type: application/json" \
-H "idempotency-key: app-key:acme" \
-d '{ "name": "acme app", "capabilities": ["read"] }'

A replay returns the original key record with "secret": null — the secret is still only ever shown at the moment of creation.

Terminal window
curl "$API/v1/brains/tenant:acme/keys" \
-H "authorization: Bearer $NICIA_KEY"
{
"data": {
"keys": [
{
"id": "key_7f3a1c",
"name": "Acme web app",
"capabilities": ["read"],
"lastUsedAt": "2026-08-14T11:58:03Z",
"status": "active",
"createdAt": "2026-08-14T09:41:02Z"
}
]
}
}

lastUsedAt is how you find the keys nobody is using any more.

The listing is keyset-paged like every other one: pass limit (up to 250) and follow nextCursor until it stops coming back. A first page is not the inventory — which matters most under the per-session pattern below, where one Brain accumulates a key per user.

Terminal window
# Rotate: new secret now, old one keeps working for the overlap window
curl -X POST "$API/v1/brains/tenant:acme/keys/key_7f3a1c/rotate" \
-H "authorization: Bearer $NICIA_KEY"
# Revoke: immediate
curl -X DELETE "$API/v1/brains/tenant:acme/keys/key_7f3a1c" \
-H "authorization: Bearer $NICIA_KEY"

Rotation takes no body: the overlap is a fixed 60 seconds, not a window you choose. It exists to cover the in-flight request while a running fleet redeploys — it does not replace writing the new secret to your secret store, and it is deliberately too short for a slow human redeploy. If you need an unbounded cutover, mint a second key instead of rotating.

Revocation takes effect on the next request, everywhere.

Never put a Brain key in a browser, a mobile app, or anything else a user can open. Two safe patterns:

Proxy through your backend. Your server holds the key, authenticates your user, and calls Nicia on their behalf. Pass a consumerId so you can later reconstruct exactly what one user was shown:

Terminal window
curl -X POST "$API/v1/brains/tenant:acme/context" \
-H "authorization: Bearer $BRAIN_KEY" \
-H "content-type: application/json" \
-d '{ "prompt": "…", "consumerId": "user:4471" }'

consumerId is a correlation label you choose. It is never authorization — it does not restrict what the call can reach, so the scoping still has to come from the key.

Mint a short-lived key per session. When a user genuinely needs to call Nicia directly:

Terminal window
-d '{ "name": "session u_4471", "capabilities": ["read"], "expiresAt": "2026-08-14T13:00:00Z" }'

Expired keys are 401 and cannot be revived.

Everything a key does is attributed to it, derived server-side from the credential rather than from anything the caller sends. Staged changes name the key that proposed them, receipts name the key that made the read, and revoking a key leaves that history intact.

{
"data": {
"author": { "kind": "key", "id": "key_7f3a1c", "name": "support-agent" }
}
}

This is why a key per workload beats a shared key: it is the difference between “an agent wrote this” and “the support triage agent wrote this on 14 August.”