Ir al contenido
↑↓Navigate↵SelectescClose

Personal Developer Keys

User-bound nxd_ credentials for the Query and Trace HTTP APIs, separate from gateway API keys

Esta página aún no está disponible en tu idioma.

Personal developer keys are user-bound credentials for CLI, SDK, and automation clients that need Nexus Query, Trace, and control-plane APIs without a browser session.

In the console they appear under Account → Personal developer keys (/account). The auth service stores them as developer access tokens and issues secrets with the nxd_… prefix.

They are not gateway API keys (nxs_…) and not trace ingest keys. Gateway keys send model traffic; personal keys act as the issuing user — reading observability data, writing request feedback, and, with management scopes, creating and revoking keys and changing settings — inside the scope the key is bound to.

On a Traefik-backed deployment, consumers and tenants reach Nexus through the published hostnames, not the internal service ports.

Public hostname (env) Typical role Example
NEXUS_CONSOLE_HOSTNAME Tenant console (UI + same-origin BFF) https://console.example.com
NEXUS_HOSTNAME Gateway edge (model traffic, /v1/traces) https://api.example.com

Auth (auth:14453) and Query (query:14461) stay on the private Compose network. The console container calls them with AUTH_HTTP_ENDPOINT and QUERY_HTTP_ENDPOINT. Stock Traefik labels publish console and gateway (plus optional Grafana / path-routed trace ingest on the gateway host). They do not publish auth or query directly.

What that means for you:

  1. Mint your first key in the console UI on the console hostname (works with OIDC or password login, depending on deployment policy). After that, a key may mint, list, and revoke your other keys itself.
  2. Send model traffic to the gateway hostname with a gateway API key (nxs_…).
  3. Call Query/Trace with nxd_… only against a Query HTTP base URL your operator has made reachable to you (published Query hostname, VPN, or private network). The stock public edge does not put Query on the gateway hostname.
  4. Manage keys and settings with nxd_… against the console hostname: the console’s control-plane and developer-key routes forward an Authorization: Bearer nxd_… header to the private services.

Browser sessions in the console do not need a personal key: the console BFF under /api/query/** forwards the signed-in session cookie to Query on the private network.

Terminal window
# Set these to your deployment's public URLs.
CONSOLE_URL="${CONSOLE_URL:-https://console.example.com}"
GATEWAY_URL="${GATEWAY_URL:-https://api.example.com}"
# Query base URL only if your operator publishes Query HTTP to clients.
# Stock Traefik compose leaves query on the private network.
QUERY_URL="${QUERY_URL:-https://query.example.com}"
Capability Available to a deployed-app user
Create / list / revoke in the console UI Yes
Create / list / revoke with another nxd_… key Yes — same user, inside the key’s binding and scopes
Org-wide, single-project, or whole-workspace binding Yes (workspace binding needs workspace manager/auditor)
Expiry (default 30 days, bounded by the effective maximum key age) Yes
Scoped permissions (see below) Yes
Authorization: Bearer nxd_… against Query HTTP (when Query is reachable) Yes
Control-plane routes through the console hostname with management scopes Yes
Use against the gateway for chat/completions No — use nxs_…
Rotate in place (edit secret) No — mint a new key, then revoke the old one
Machine / service-account tokens (non-user) No
Create workspaces or accept invitations No — those need a console session
Call Query via the public gateway hostname No (gateway does not serve /api/requests, …)
Call Query via console /api/query/** with an nxd_… bearer No — that BFF uses the browser session cookie

Effective access on every use is the intersection of:

  1. Scopes stored on the token
  2. The token’s binding (workspace, organization, or project)
  3. The user’s current org/project/workspace membership and role
  4. Token not revoked or expired; user still active
  5. Policy (PDP) allow for the specific read or feedback action

Demotion, membership removal, disablement, expiry, or revocation takes effect on the next verification. Services do not cache a verified personal key, so the next request after any of those changes sees it. Personal keys are not subject to console session idle timeout.

Every key is bound to exactly one scope, chosen at creation:

Binding Create with Reaches
Organization org_id That organization and its projects, at your role there (including manager or auditor inherited from its workspace).
Project org_id + project_id That project only. An organization-wide role applies to the project, never to the organization itself.
Workspace workspace_id That workspace and every organization in it, at your workspace role. Requires workspace manager or auditor.

Anything outside the binding answers 403, even when your console session could reach it. An organization or project binding never carries workspace authority: workspace limits, members, wallet, and invitations need a workspace binding or a session.

The console scope picker offers these values. Creation rejects any scope the caller’s effective role does not hold.

Scope Console label Unlocks
query:read Request metadata Request rows, sessions, session timelines
trace:read Trace metadata Trace envelopes and safe tool summaries
trace_payload:read Full trace payloads Stored bodies and full tool payload refs when tracing captured them
metrics:read Metrics Usage, metrics overview, panel exports
feedback:write Feedback Submit request feedback from SDKs or automation

Management scopes, unchecked by default in the console:

Scope Console label Unlocks
control_plane:read Organization read List organizations, projects, members, and key metadata; create and revoke gateway keys you issue
control_plane:write Organization write Settings changes, other members’ keys, trace ingest keys, and the other control-plane mutations your role allows
provider_keys:write Provider credentials Create and revoke provider connections (secrets are write-only)
model_registry:read Model registry Models, endpoints, and pricing
audit:read Audit Audit events

A management scope never widens your role: every control-plane route still applies its own role check (for example, provider connections need project manager) inside the key’s binding.

Role ceilings for mintable scopes:

Role Mintable scopes
manager Every scope above
auditor query:read, trace:read, metrics:read, feedback:write, control_plane:read, model_registry:read, audit:read
user query:read, trace:read, metrics:read, feedback:write, control_plane:read, model_registry:read

The role is the one you hold at the key’s binding: your project role for a project binding, your organization role for an organization binding, and your workspace role for a workspace binding. On every use the stored scopes are intersected with your current role there, so a demotion removes scopes from existing keys.

Full payload reads also require a successful trace_payload.read PDP decision and stored payload references. Metadata reads remain available with query:read, trace:read, or metrics:read as appropriate.

  • Generated secrets — nxd_…, gateway nxs_…, and trace ingest keys — are returned only in the response that creates them (and, for trace ingest keys, the rotate response). List and revoke responses carry metadata only, and there is no route that reveals a stored secret.
  • Provider secrets are write-only. Create accepts secret; the create response and every listing return connection metadata (id, provider, name, base URL, expiry, revoked) and never the secret.
Prefix Purpose Where you send it
nxd_… Personal developer key (this doc) Query HTTP base URL (when published / reachable); console hostname for management routes
nxs_… Gateway API key Gateway hostname (/v1/…, /anthropic/…)
Trace ingest key Project-scoped OTLP write Gateway hostname /v1/traces (path-routed)

Query HTTP rejects gateway API keys with HTTP 401 (gateway api keys cannot read query APIs).

  1. Open the tenant console on your deployment’s console hostname (for example https://console.example.com).
  2. Sign in (OIDC and/or password, per that deployment’s auth config).
  3. Open Account (/account).
  4. Select the organization, then New key.
  5. Set name, optional project scope (or, as a workspace manager or auditor, Bind to workspace), expiry days, and permissions. The permission list shows only the scopes your role allows at that binding. The expiry field defaults to the lesser of 30 days and the effective maximum key age for the selected workspace, organization, or project, and the dialog shows that maximum when one is configured.
  6. Copy the secret once — it is not shown again.

The raw secret is returned only on create. The Account list is a searchable, paginated table that defaults to Active (hides revoked and past-expiry keys); switch the state filter to Revoked / Expired / All when needed. For the selected organization it lists keys bound to that organization or its projects, plus keys bound to its workspace. List rows show name, prefix, scope, permissions, status (Active / Expires in N days / Expired / Revoked), and last-used time. Revoke stays available for every status except already-revoked keys.

This is the path that works on OIDC-only deployments (password login disabled).

Use the personal key as a bearer token against Query HTTP, not against the gateway hostname.

Every list/detail route requires org_id (and respects optional project_id filters and the token’s project grant).

Terminal window
QUERY_URL="${QUERY_URL:?set QUERY_URL to the Query HTTP base your operator published}"
NEXUS_DEV_TOKEN="nxd_…" # secret shown once at create time
ORG_ID="org_…"
# Recent requests
curl -sS "$QUERY_URL/api/requests?org_id=$ORG_ID&limit=20" \
-H "Authorization: Bearer $NEXUS_DEV_TOKEN" | jq .
# One request
REQUEST_ID="req_…"
curl -sS "$QUERY_URL/api/requests/$REQUEST_ID?org_id=$ORG_ID" \
-H "Authorization: Bearer $NEXUS_DEV_TOKEN" | jq .
# Sessions
curl -sS "$QUERY_URL/api/sessions?org_id=$ORG_ID&limit=20" \
-H "Authorization: Bearer $NEXUS_DEV_TOKEN" | jq .
# Session timeline
SESSION_ID="sess_…"
curl -sS "$QUERY_URL/api/sessions/$SESSION_ID/timeline?org_id=$ORG_ID" \
-H "Authorization: Bearer $NEXUS_DEV_TOKEN" | jq .
# Metrics overview (needs metrics:read)
curl -sS "$QUERY_URL/api/metrics/overview?org_id=$ORG_ID" \
-H "Authorization: Bearer $NEXUS_DEV_TOKEN" | jq .
# Trace metadata for a request (needs trace:read)
curl -sS "$QUERY_URL/api/traces/requests/$REQUEST_ID?org_id=$ORG_ID" \
-H "Authorization: Bearer $NEXUS_DEV_TOKEN" | jq .
# Stored body (needs trace_payload:read + stored body + PDP allow)
curl -sS "$QUERY_URL/api/body/by-request/$REQUEST_ID?org_id=$ORG_ID&kind=response" \
-H "Authorization: Bearer $NEXUS_DEV_TOKEN"
# Feedback (needs feedback:write)
curl -sS -X POST "$QUERY_URL/api/requests/$REQUEST_ID/feedback" \
-H "Authorization: Bearer $NEXUS_DEV_TOKEN" \
-H 'Content-Type: application/json' \
-d "$(jq -n \
--arg org "$ORG_ID" \
--arg project "$PROJECT_ID" \
'{org_id:$org, project_id:$project, score:1, label:"helpful", comment:"worked well"}')"

score must be -1, 0, or 1.

Requires query:read and membership for the org/session. Project-scoped keys only see events for their project.

Terminal window
curl -N "$QUERY_URL/api/sessions/$SESSION_ID/live?org_id=$ORG_ID" \
-H "Authorization: Bearer $NEXUS_DEV_TOKEN" \
-H 'Accept: text/event-stream'
import os
import urllib.parse
import urllib.request
query_url = os.environ["QUERY_URL"] # operator-published Query HTTP base
token = os.environ["NEXUS_DEV_TOKEN"] # nxd_…
org_id = os.environ["ORG_ID"]
params = urllib.parse.urlencode({"org_id": org_id, "limit": 20})
req = urllib.request.Request(
f"{query_url}/api/requests?{params}",
headers={"Authorization": f"Bearer {token}"},
)
with urllib.request.urlopen(req) as resp:
print(resp.read().decode())
const queryUrl = process.env.QUERY_URL!; // operator-published Query HTTP base
const token = process.env.NEXUS_DEV_TOKEN!; // nxd_…
const orgId = process.env.ORG_ID!;
const url = new URL("/api/requests", queryUrl);
url.searchParams.set("org_id", orgId);
url.searchParams.set("limit", "20");
const response = await fetch(url, {
headers: { Authorization: `Bearer ${token}` },
});
if (!response.ok) {
throw new Error(`${response.status} ${await response.text()}`);
}
console.log(await response.json());

When the token carries the required scope(s) and Query HTTP is reachable, these routes accept nxd_… bearers:

Method Path Typical scopes
GET /api/requests query:read
GET /api/requests/:request_id query:read
GET /api/sessions query:read
GET /api/sessions/:session_id/timeline query:read
GET /api/sessions/:session_id/live query:read
GET /api/usage metrics:read
GET /api/metrics/overview metrics:read
GET /api/exports/panel metrics:read
POST /api/requests/:request_id/feedback feedback:write
GET /api/body/by-request/:request_id trace_payload:read
GET /api/traces/requests/:request_id trace:read (+ payload scope for full refs)
GET /api/traces/sessions/:session_id trace:read
GET /api/traces/tools/:request_id trace:read
GET /api/traces/:trace_id/tree trace:read
GET /api/traces/:trace_id/spans/:span_id trace:read
GET /api/traces/:trace_id/artifacts/:artifact_id trace:read / trace_payload:read as applicable
GET /api/requests/:request_id/trace trace:read
GET /api/sessions/:session_id/traces trace:read
GET /api/sessions/:session_id/traces/live trace:read

Canonical route descriptions live in ../architecture/wire-contracts.md.

GET /api/reports/workspace requires query:read, metrics:read, and control_plane:read on the key, and refuses project-bound keys.

The console hostname forwards an Authorization: Bearer nxd_… header on every route that proxies the control plane (for example /api/orgs/**, /api/projects/**, /api/workspaces/**, /api/api-keys/**, /api/trace-ingest-keys/**, /api/provider-keys/**, and /api/models/**), on file and vector-store routes under /api/artifacts/orgs/:org/projects/:project/…, on /api/users/lookup, and on its developer-key routes (/api/auth/developer-tokens…). The console’s Query proxies (/api/query/**), session routes (/api/session/**), and /api/audit/request-viewed use only the browser session cookie. A request carrying a developer key never uses the browser session cookie. The control plane applies the same handlers and role checks a console session gets, clipped to the key’s binding, so each route needs its usual scope and role plus a binding that contains the target.

Terminal window
CONSOLE_URL="${CONSOLE_URL:-https://console.example.com}"
NEXUS_DEV_TOKEN="nxd_…" # bound to the org, with control_plane:read
ORG_ID="org_…"
PROJECT_ID="proj_…"
# Mint a gateway key for a project (the nxs_ secret is in this response only).
curl -sS -X POST "$CONSOLE_URL/api/api-keys" \
-H "Authorization: Bearer $NEXUS_DEV_TOKEN" \
-H 'Content-Type: application/json' \
-d "$(jq -n --arg org "$ORG_ID" --arg project "$PROJECT_ID" \
'{org_id: $org, project_id: $project, name: "ci-gateway"}')"
# List gateway keys (metadata only).
curl -sS "$CONSOLE_URL/api/api-keys?org_id=$ORG_ID&project_id=$PROJECT_ID" \
-H "Authorization: Bearer $NEXUS_DEV_TOKEN" | jq .
# Revoke one.
curl -sS -X DELETE "$CONSOLE_URL/api/api-keys/ak_…" \
-H "Authorization: Bearer $NEXUS_DEV_TOKEN" -o /dev/null -w '%{http_code}\n'

Provider connections need provider_keys:write on the key and project manager authority; the secret is accepted on create and never returned:

Terminal window
curl -sS -X POST "$CONSOLE_URL/api/provider-keys" \
-H "Authorization: Bearer $NEXUS_DEV_TOKEN" \
-H 'Content-Type: application/json' \
-d "$(jq -n --arg org "$ORG_ID" --arg project "$PROJECT_ID" --arg secret "$OPENAI_API_KEY" \
'{org_id: $org, project_id: $project, provider: "openai", name: "primary", secret: $secret}')"

Routes that act outside any single scope — creating a workspace and accepting an invitation — answer 403 for a developer key.

Personal keys are minted, listed, and revoked through /api/auth/developer-tokens on the console origin. The caller is either a console session or another nxd_ key belonging to the same user. A key acting as the caller:

  • mints only keys bound inside its own binding (its workspace or an organization in it; its organization or a project in it; its own project),
  • may request only scopes it holds itself, still capped by your role at the new binding,
  • lists and revokes only keys bound inside its own binding, including itself.

Mint your first key in the console (or with a console session), then rotate from automation: mint the replacement with the current key, switch to it, and revoke the old one.

Terminal window
CONSOLE_URL="${CONSOLE_URL:-https://console.example.com}"
NEXUS_DEV_TOKEN="nxd_…"
ORG_ID="org_…"
# Create a narrower key with an existing key.
curl -sS -X POST "$CONSOLE_URL/api/auth/developer-tokens" \
-H "Authorization: Bearer $NEXUS_DEV_TOKEN" \
-H 'Content-Type: application/json' \
-d "$(jq -n \
--arg org "$ORG_ID" \
--arg name "CI observability reader" \
'{
org_id: $org,
project_id: null,
name: $name,
scopes: ["query:read","trace:read","metrics:read","feedback:write"],
expires_at: "2026-12-31T00:00:00Z"
}')"

Successful create response shape:

{
"id": "dt_…",
"token": "nxd_…",
"prefix": "…",
"workspace_id": null,
"org_id": "org_…",
"project_id": null,
"scopes": ["query:read", "trace:read", "metrics:read", "feedback:write"],
"expires_at_unix": 1770000000
}

Store token immediately. Only the hash and prefix are persisted.

Project-scoped create:

Terminal window
curl -sS -X POST "$CONSOLE_URL/api/auth/developer-tokens" \
-H "Authorization: Bearer $NEXUS_DEV_TOKEN" \
-H 'Content-Type: application/json' \
-d "$(jq -n \
--arg org "$ORG_ID" \
--arg project "$PROJECT_ID" \
--arg name "CI project reader" \
'{
org_id: $org,
project_id: $project,
name: $name,
scopes: ["query:read","metrics:read"],
expires_at: "2026-12-31T00:00:00Z"
}')"

Workspace-bound create sends workspace_id instead of org_id and project_id; it requires workspace manager or auditor:

Terminal window
curl -sS -X POST "$CONSOLE_URL/api/auth/developer-tokens" \
-H "Authorization: Bearer $NEXUS_DEV_TOKEN" \
-H 'Content-Type: application/json' \
-d "$(jq -n --arg workspace "$WORKSPACE_ID" \
'{workspace_id: $workspace, name: "workspace automation", scopes: ["control_plane:read"]}')"

expires_at is an optional RFC3339 timestamp. When it is omitted, the token expires after 30 days. A workspace, organization, or project maximum key age — or NEXUS_MAX_KEY_AGE_DAYS — caps the requested timestamp and lowers the 30-day default; a request beyond the cap is rejected with HTTP 400 and {"message":"key expiration exceeds the maximum key age"}. With no cap configured, any future timestamp is accepted. Create also returns 400 with a message for empty names, scopes the caller’s role cannot mint, scopes the calling key does not hold, or a project outside the organization, and 403 with a message when the caller has no membership in the requested organization, lacks workspace authority for a workspace binding, or is a key minting outside its own binding. Those validation failures do not invalidate the caller’s session or key.

List and revoke (omit org_id to list every binding; workspace_id filters workspace-bound keys):

Terminal window
curl -sS "$CONSOLE_URL/api/auth/developer-tokens?org_id=$ORG_ID" \
-H "Authorization: Bearer $NEXUS_DEV_TOKEN" | jq .
TOKEN_ID="dt_…"
curl -sS -X DELETE "$CONSOLE_URL/api/auth/developer-tokens/$TOKEN_ID" \
-H "Authorization: Bearer $NEXUS_DEV_TOKEN" \
-o /dev/null -w '%{http_code}\n'

A console session cookie works on the same paths in place of the bearer header.

Auth also exposes the same lifecycle paths on its private HTTP listener (POST|GET /api/auth/developer-tokens, DELETE /api/auth/developer-tokens/{token_id}) for operators with network access to that service.

Situation Typical result
Missing / bad bearer on Query HTTP 401
Gateway nxs_… key on Query HTTP 401
Hitting /api/requests on the gateway hostname HTTP 404 (not a Query route there)
Scope missing for the route HTTP 403
Org/project outside membership or token grant HTTP 403
Control-plane target outside the key’s binding HTTP 403
Workspace creation or invitation accept by key HTTP 403
Requested create scopes exceed role HTTP 400 {"message":"scope … is not available to your role"}
Key requests a scope it does not hold HTTP 400 {"message":"scope … is not held by this developer key"}
Key mints outside its own binding HTTP 403 {"message":"a developer key cannot mint a key outside its own binding"}
Workspace binding without workspace authority HTTP 403 {"message":"workspace developer keys require a workspace manager or auditor role"}
Create expiry exceeds maximum key age HTTP 400 {"message":"key expiration exceeds the maximum key age"}
Create without membership in the org HTTP 403 {"message":"no membership in organization"}
Revoke unknown, already-revoked, or out-of-binding token HTTP 404 {"message":"developer token not found"}
Token revoked or expired Verification fails
User disabled or membership removed Verification fails / 403
Full body when tracing did not store it HTTP 404 (body not stored)

Create and revoke emit developer_token.created and developer_token.revoked, with the binding (workspace_id, org_id, project_id) and, when another key made the call, acting_developer_token_id. Verified principals carry an internal developer_token:<id> scope for correlation, and control-plane audit events for actions a key takes name the issuing user as actor. Query reads emit data_access.* events (for example data_access.request_viewed, data_access.trace_payload_viewed) that include the token id without the raw secret. last_used_at updates on successful verification.

When you run the stock local Compose stack with published host ports (see ../reference/ports.md), the same APIs are also reachable as http://127.0.0.1:14449 (console), http://127.0.0.1:14453 (auth), and http://127.0.0.1:14461 (query). Prefer the deployed hostname model above when writing client integrations; use localhost only for contributor debugging on a machine that can reach those ports.