Skip to content

Authentication

Every request carries a bearer token:

Authorization: Bearer <credential>

There are two kinds of credential, and the choice is about who is acting, not about convenience.

User token API key
Represents a person a system
Reads everything its role allows cases, documents and decisions only
Looks like a JWT from your sign-in service ug_live_… or ug_test_…
Comes from OAuth 2.0 against your deployment’s sign-in service Admin → Developers in the dashboard
Carries a role a set of scopes
May review a decision yes no

A machine integration usually wants an API key. Use a user token when the caller genuinely is a person — for example, when your product signs someone in and then acts strictly on their behalf.

A workspace admin mints a key in the dashboard, under Admin → Developers.

  • The key is shown once. It cannot be read back afterwards. Put it where your servers can read it and a browser cannot, and mint a new one rather than trying to recover an old one.
  • ug_live_ and ug_test_ authenticate identically. The tag exists so that a key pasted into the wrong place is recognisable at a glance.
  • A key may carry an expiry. From that instant it stops authenticating; there is no grace period.
  • Give a key the least it needs. A key with no scopes reads the cases, documents and decisions described below and changes nothing. Scopes are not addable later, so a key that needs more is a new key.

Any key can read the cases, documents and decisions in this workspace. It can never read your people, your workspace settings, your sign-on setup, your operator-access records, the audit export or your keys — no scope grants any of those, because they are administrative acts of a person and the record has to name one.

Scopes are about what a key may change. Reading the resources above needs no scope at all, which is why a key with none is a read-only key.

Scope Lets the key
scenarios:write create cases, attach and finalise documents, start runs
guidelines:write upload and manage guideline sources
question_sets:write author, edit and publish question sets
document-types:write manage the workspace’s document-type vocabulary
notifications:write create and change task-notification subscriptions and signing secrets
embed:mint mint an embed handoff for one case

An unknown scope is rejected when the key is created, rather than accepted into a key that silently authorises nothing.

These are acts of a person, and they are refused to an API key at every scope — for reading as well as for writing — because the record has to name a human:

  • reviewing a decision, and overriding an answer — the sign-off that makes a proposal final;
  • membership — invitations, and the workspace roster;
  • the workspace’s own administration — settings, sign-on configuration, operator-access records, the audit export, and the keys themselves.

Get an access token from the sign-in service your deployment is configured against: authorization code with PKCE for a person, client credentials for a machine user. Send it verbatim.

Three rules catch out almost everyone:

  1. Send the access token, not the ID token. A token carrying ID-token markers (at_hash, nonce) is refused.

  2. Include this scope, exactly. It is a constant — the same literal string for every caller of every deployment, with nothing to substitute in:

    urn:zitadel:iam:user:resourceowner

    It asks the sign-in service to state which organization your subject belongs to, which is checked against the organization your workspace is bound to. Send it from the first request: it is required once your workspace is bound, and harmless before that.

  3. Nothing else in the token is read. Exactly three things reach a decision: sub, which must resolve to a known user; iss, which must be your deployment’s issuer; and the organization, when the scope above was sent. Extra claims never break a request and never grant anything. No claim selects a workspace or sets a role — both follow from sub.

A user principal carries one role, and each contains the one below it.

Role Can
viewer read everything in the workspace
reviewer everything above, plus write resources, run cases, and review decisions
admin everything above, plus membership, settings, identity configuration and API keys

An expired token, a bad signature, an unknown subject and a wrong organization all return the same response: 401, urn:ug:error:unauthorized, a WWW-Authenticate: Bearer header, and an identical body. They are deliberately not told apart, so the API cannot be used to probe which of them it was.

{
"type": "urn:ug:error:unauthorized",
"title": "Missing or invalid credential",
"status": 401,
"detail": "credential does not correspond to a known user",
"instance": "/v1/scenarios",
"request_id": "0198c5f2-1c2e-7c9b-9f10-2a3b4c5d6e7f"
}

The request_id is what support correlates against the log that does say. Keep it.

A 403 means something different and is worth reading as such: the credential is valid and re-presenting it will not help. Either the principal lacks the role or scope, or it reached across into another workspace.

If tokens that used to work start failing and nothing else changed, check the scope in rule 2 first. It is the single most common cause.

Keep credentials out of everything but the header

Section titled “Keep credentials out of everything but the header”

Never put a token or an API key in a URL, a log line, a bug report or a shared example. A URL ends up in browser history, in proxy logs and in referrer headers; the Authorization header does not.

  • Quickstart — a worked path from a credential to a running underwrite.
  • Conventions — errors, pagination, idempotency and rate limits.
  • API reference — every route, request and response.