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.

{
"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; neither is on the Brain-key surface today. The department above reads because it also holds read. Creating and deleting a Brain stay with IT’s management key; no Brain key reaches them. 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" \
-d '{ "name": "acme app", "capabilities": ["read"], "idempotencyKey": "app-key:acme" }'

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.

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" \
-H "content-type: application/json" \
-d '{ "overlapSeconds": 3600 }'
# Revoke: immediate
curl -X DELETE "$API/v1/brains/tenant:acme/keys/key_7f3a1c" \
-H "authorization: Bearer $NICIA_KEY"

Rotation exists so a running fleet does not drop requests while it redeploys. 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:

const { data } = await nicia.context(brainKey, {
prompt: question,
consumerId: `user:${session.userId}`,
});

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