Three status codes account for almost every authentication problem, and they mean genuinely different things:

Every 401 looks identical

A 401 returns the same body regardless of cause. Missing header, malformed credential, unknown prefix, wrong secret, revoked key, expired key, API key presented in a cookie — all the same response. A test compares them byte for byte. That is deliberate. Distinguishing them tells an attacker which part of a forged credential to fix next, which turns the endpoint into an oracle. The reason is classified internally for metrics and logs — where it is useful to an operator and not reachable by the person attacking you. The consequence for you is that a 401 must be diagnosed from your side. Work through this list in order.

1. Is the credential in the right place?

Authorization: Bearer twk_…. Not X-API-Key, not a query parameter, not a cookie. The most common mechanical mistakes: an unset shell variable expanding to Bearer with nothing after it, a copied secret carrying a trailing newline, and a truncated paste. The credential is a fixed shape — twk_ then 12 characters, then an underscore, then 52 characters, all base32 (A–Z and 2–7). Anything else fails at the parser before a lookup happens.

2. Has the key expired?

Expiry is mandatory — there is no “never expires” key. The default is 90 days from creation and the maximum is 365, so a key issued at the start of a project stops working partway through it, by design. A credential nobody rotates is the problem this prevents. List your keys through a browser session to see expiry dates:
The listing shows every key the organisation has ever issued, including revoked and expired ones, because “what has this organisation ever issued” is the first question after an incident. Match on the prefix — the public 12-character portion — which is safe to log and safe to paste into a ticket.

3. Has the key been revoked?

Revocation takes effect on the next request, and revoked rows are never deleted, so a revoked key appears in the listing with its revocation recorded. If someone rotated credentials and one deployment was missed, this is what it looks like. There is no un-revoking. Issue a new key. A credential beginning twk_ presented as the trustivan_session cookie is refused outright, even when it is a genuine, valid, unexpired key. This surprises people, so it is worth stating why rather than just that. A cookie is ambient: the browser attaches it to any request to this origin, including requests a hostile page caused. An API key deliberately carries no CSRF protection precisely because it is not ambient. Honouring one from a cookie would create a credential that is both ambient and unprotected — worse than either mechanism on its own. In practice this shows up when a client library is configured with a cookie jar and something writes the key into it, or when a test harness sets both. Send it in the Authorization header only.

5. Are you using a refresh token at the API?

Only access tokens are accepted at API endpoints. A refresh token presented to /api/v1/... is rejected, so it cannot extend a session past the access-token lifetime or survive a revocation. Access tokens last 15 minutes by default; use /api/v1/auth/refresh to obtain a new one. The dashboard and the NHI Security console do this for you: a request that meets an expired session renews it once and is repeated, and only a refused renewal sends you to sign in.

6. Are you being rate limited on the auth endpoints?

Sign-in, sign-up and refresh are limited to 10 requests per minute per IP address. A 429 here rather than a 401 usually means a script retrying a failed login in a tight loop, or several developers behind one egress address. Wait a minute; do not retry harder. Authenticated requests have a separate, much more generous per-credential limit, counted against the credential rather than the organisation — so one runaway job cannot lock out colleagues. Symptom. A browser-session request with POST, PATCH, PUT or DELETE is refused, while GET on the same resource works. Cookie sessions require a double-submit CSRF token on unsafe methods: the value of the trustivan_csrf cookie, echoed in an X-CSRF-Token header.
The CSRF cookie is deliberately readable — it has to be, since the client must echo it — while the session cookie is HttpOnly and is not. A hostile page can cause your browser to send the session cookie but cannot read the CSRF cookie to construct the matching header. Bearer requests never need this header, and adding one is harmless but pointless. If you are writing a machine client and reaching for a CSRF token, you are probably using a session where an API key would be simpler.

A 403, and how to read it

A 403 means your credential is valid and belongs to this tenant, and lacks a permission. Unlike a 401, it tells you exactly what is missing:
That disclosure is safe: you already know the tenant is yours, and a machine that cannot be told which scope it lacks is a machine whose operator has to guess at the permission model.
  • required_scope is the exact scope name to add.
  • principal is api_key or user, which tells you which credential was actually used. If you expected api_key and see user, a cookie jar is authenticating you instead of your key.
A key’s scopes are fixed at creation. There is no endpoint that widens an existing key, deliberately: a credential whose authority can grow is a credential whose blast radius is unbounded after it leaks. Issue a new key with the scopes you need, deploy it, then revoke the old one — both work during the overlap. See Rotate an API key. Note also that a key does not inherit its creator’s role, and does not track it. If a key stopped working after somebody’s permissions changed, that is not the cause; the key’s authority is exactly what it was granted, which is why a demotion cannot silently break a pipeline and a promotion cannot silently widen a key.

The scope you want may not be grantable at all

Three permissions can never be attached to a machine credential: Requesting one at creation is a 400 naming the scope, not a silently narrowed key — a key that is quietly less capable than you asked for surfaces as a 403 in a pipeline weeks later. These actions require a human session. Ask the deployment for the authoritative grantable list rather than trusting a copy:
The full table, with the reasoning for each, is at Scopes.

A 404 that is really “not your tenant”

Symptom. A GET on an ID you are certain exists returns 404. “Does not exist” and “exists, but is not yours” return the same status and the same body, and a test asserts they match exactly. A 403 would confirm the resource exists and merely belongs to someone else, which turns a UUID guess into reconnaissance about who else is a customer. So the ambiguity is the feature, and it applies everywhere — including bulk operations, which report not_found for another tenant’s finding. In practice, this usually means one of:
  • The ID came from a different environment. Staging IDs do not resolve in production.
  • The ID came from a different organisation. A request acts in the organisation selected in the organisation menu (or with PUT /api/v1/me/active-org), and an API key in the organisation that issued it — see Tenant isolation.
  • The resource was deleted.
  • The ID is simply wrong — a truncated copy, or an asset ID used where a finding ID was expected.
To confirm, list the collection rather than guessing at IDs. A listing returns what is yours, so an ID absent from your own listing is either not yours or not there.

Where to go next