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

WhereSettings → API keys
Key API routesGET /settings/api-keys, POST /settings/api-keys, POST /settings/api-keys/revoke
Formatnk_live_ + 32 hex characters
Who can createMembers and admins (viewers cannot); a member may create a writer/reader key but only an admin may create an admin-role key
Who can revokeThe person who created the key, or any admin — you can only revoke a key you created (admins can revoke any)
Used byThe ingest endpoints — see Send traces
Secret handlingShown 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.

PrefixWhat it isWhere it comes fromWhat it authenticates
nk_live_Agent (ingest) API key — the machine credentialSettings → API keys (POST /settings/api-keys), shown onceIngest (/v1/traces, /ingest/*) and agent-scoped programmatic calls: the eval CLI, the MCP server, read APIs
nk_sess_Human login session token — opaque, short-livedMinted when a person signs in to the appThe 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-adminIssued 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.json

Any 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."}
# 403

Routes 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 stored prefix (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.create and api_key.revoke.