API keys
An API key is the credential your agents and collectors use to send traces into a Neens
agent. Every key is a nk_live_… bearer token minted for one agent: presenting it
resolves the request to that agent (and its company) unambiguously, so a trace can never
land in the wrong workspace.
At a glance
| Where | Settings → API keys |
| Key API routes | GET /settings/api-keys, POST /settings/api-keys, POST /settings/api-keys/revoke |
| Format | nk_live_ + 32 hex characters |
| Who can create | Members and admins (viewers cannot); a member may create a writer/reader key but only an admin may create an admin-role key |
| Who can revoke | The person who created the key, or any admin — you can only revoke a key you created (admins can revoke any) |
| Used by | The ingest endpoints — see Send traces |
| Secret handling | Shown once at creation; Neens stores only a hash and the display prefix |
The three key prefixes
Every bearer credential in the product announces what it is in its first few characters. The prefix is how you tell at a glance which credential you’re holding — and the server dispatches on it too, so pasting the wrong kind into a config never silently half-works.
| Prefix | What it is | Where it comes from | What it authenticates |
|---|---|---|---|
nk_live_ | Agent (ingest) API key — the machine credential | Settings → API keys (POST /settings/api-keys), shown once | Ingest (/v1/traces, /ingest/*) and agent-scoped programmatic calls: the eval CLI, the MCP server, read APIs |
nk_sess_ | Human login session token — opaque, short-lived | Minted when a person signs in to the app | The app’s own requests on behalf of that user, with that user’s role and org scope |
nk_admin_ | Control-plane admin key — operator / company-admin | Issued by the control plane (a company’s initial admin key is created when the company is provisioned) | The operator /admin/* control plane. It carries no agent scope, so it must never be used for ingest |
Details you rarely need: an agent key is nk_live_ plus 32 hex characters, and only its first
11 characters are stored for display alongside a SHA-256 hash. A session token carries a longer
random tail and is bound to the session’s absolute lifetime (7 days) and its sliding idle
window (1440 minutes). An admin key’s prefix also encodes its scope —
nk_admin_op_… for an operator key, nk_admin_co_… for a company-admin key.
Only a nk_live_ agent key pins the destination agent. Ingest resolves the agent from
the authenticated agent key first; a credential of any other kind leaves that unresolved, so the
trace falls through to the X-Neens-Project-Id header, then a project_id in the body, then the
bootstrap agent — which is how a trace ends up in a workspace you weren’t looking at. Send
traces with the agent key and that can’t happen. A credential that doesn’t resolve at all is a
401, never a silent redirect.
Create a key
Open Settings → API keys
Make sure the agent you want to ingest into is the active agent — a key is created under, and scoped to, the current agent.
Name it and generate
Click New key in the top right to open the create dialog. Give the key a name that identifies
the caller (e.g. CI pipeline, Production ingest), pick a role (writer is the default for
ingest), and click Generate key (POST /settings/api-keys).
Copy the key immediately
The full nk_live_… value is returned once, in the create response, with the banner
“New key created — copy it now, it won’t be shown again.” Neens stores only a SHA-256
hash plus the first few characters for display — the full key is not retrievable later,
by anyone. If you lose it, revoke it and mint a new one.
Worked example: mint a key, then use it
The UI button calls the same endpoint you can call yourself. Minting takes an authenticated
caller with permission to manage API keys (your session, or an admin key) plus a name and an
optional role:
curl -X POST https://your-neens-host/settings/api-keys \
-H "Authorization: Bearer nk_sess_..." \
-H "Content-Type: application/json" \
-d '{"name": "CI pipeline", "role": "writer"}'Minting an operator-scoped key needs a fresh second factor. A key is non-interactive: it can
never be challenged for a code, which would make key issuance a quiet way to hold operator access
that two-factor authentication no longer
covers. So when the caller is an operator with MFA enabled, the mint requires a session that proved
a second factor in the last 15 minutes — or a current code sent as
"mfaCode" alongside name and role. Without either, the response is
403 {"detail": {"reason": "mfa_reauth_required"}}. Ordinary agent keys minted by a company
member are unaffected.
The response carries the secret exactly once, as rawKey:
{
"key": {
"id": "key_9f2c1a7b4e05",
"name": "CI pipeline",
"prefix": "nk_live_9f2",
"role": "writer",
"createdAt": "2026-01-14T09:31:02.118431+00:00",
"lastUsedAt": null
},
"rawKey": "nk_live_9f2c1a7b4e05d8..."
}Copy rawKey into your secret store — every later read (GET /settings/api-keys) returns only
the prefix. Then present it as a bearer token:
curl -X POST https://your-neens-host/v1/traces \
-H "Authorization: Bearer nk_live_9f2c1a7b4e05d8..." \
-H "Content-Type: application/json" \
-d @traces.jsonAny OpenTelemetry exporter works the same way — set the endpoint to your host’s /v1/traces
and the Authorization header to the key. No X-Neens-Project-Id is needed: the key already
resolves to one agent.
See Send traces for the full set of ingest endpoints and SDK/collector setups.
What a key resolves to
A nk_live_… key is registered in two places when it’s minted: the agent’s own key list,
and a global registry that maps the key’s hash to its company + agent. On every ingest
request, Neens resolves the key to exactly that pair and stamps the traces accordingly:
- The key is the scope — a caller holding a key cannot read or write any other agent or company, and headers can’t override what the key resolves to.
Reads are scoped too, including by id
The scope is not only a write-side rule. A nk_live_… key reads only its own agent, and
that holds whether you ask for a list or name a single object by id. Fetching another agent’s
session, topic, enrichment run or eval run by id is refused even when you know the id exactly:
# Allowed — a session in the key's own project.
curl -s -o /dev/null -w '%{http_code}\n' \
-H "Authorization: Bearer nk_live_9f2c1a7b4e05d8..." \
https://your-neens-host/api/sessions/<id-in-the-keys-project>
# 200
# Denied — a session in a SIBLING project of the same company.
curl -s -w '\n%{http_code}\n' \
-H "Authorization: Bearer nk_live_9f2c1a7b4e05d8..." \
https://your-neens-host/api/sessions/<id-in-another-project>
# {"detail":"You do not have access to this project."}
# 403Routes that filter the agent into the lookup itself answer 404 rather than 403 — an
out-of-scope id is deliberately indistinguishable from one that does not exist, so the response
cannot be used to probe which ids are real in an agent you cannot see. Either way no data
crosses the boundary. This applies identically to the MCP server, whose tools
run against these same endpoints.
- An invalid, edited, or revoked key is always rejected with
401— it is never silently accepted or misrouted into a default agent. - A request with no credential at all is rejected with
401— ingest on this deployment requires an agent key, never a silent redirect into a default agent.
The key list (GET /settings/api-keys) shows each key’s name, prefix (nk_live_xxx…), role,
who created it, creation time, and last-used time — never the key itself.
Revoke a key
Click Revoke next to the key (POST /settings/api-keys/revoke). Revocation removes the
key from the list and stops it resolving; subsequent requests with it get
401 Invalid or revoked API key.
You can revoke a key you created; an admin can revoke any key. The Revoke control only appears on keys you’re allowed to revoke — a member never sees it on an admin’s (or another member’s) key, so admin-created keys are protected.
When does it actually stop working? Neens caches key→identity resolutions briefly to keep the ingest path fast, and a revoke immediately invalidates that cache:
- On deployments with a shared (Redis) cache — the recommended production setup — the invalidation reaches every server process at once, so the key stops working immediately.
- On deployments using a per-process in-memory cache with multiple server processes, another process may still accept the key for up to the auth-cache TTL — 60 seconds.
- With caching disabled, revocation is immediate everywhere.
Revocation cannot be undone, and a revoked key’s value cannot be reused or recovered. Mint a replacement key first if the caller needs uninterrupted ingest, then revoke the old one.
How it works
- The raw key is generated server-side (
nk_live_+ 32 random hex characters) and hashed with SHA-256 before storage; the storedprefix(the first 11 characters) exists only so you can tell keys apart in the list. - The Last used column is updated as the key is used; because warm keys are served from the auth cache, it can lag by up to one cache TTL.
- Key lifecycle events are recorded to the audit log as
api_key.createandapi_key.revoke.
Related
- Send traces — the ingest endpoints the key authenticates.
- Workspace, orgs & agents — how agents scope your data.
- Audit log — who created or revoked which key, and when.