Skip to main content

An API key returns 403 when you expect 200

Eight different refusals share two status codes. Read the body: each string means something different, and three of them are not about permissions at all.

Written for whoever runs IT10 min readUpdated

Your integration worked yesterday, or worked in testing, and now the API refuses it. Eight distinct conditions in this platform produce a 401 or a 403 for an API key, and the status code alone will not tell you which. Read the body. Each string means something different, and three of them are not about permissions at all.

Every refusal an API key can meet, what it actually means, and the fix. Work down this table in order.
ResponseWhat it meansFix
401 missing authorization headerNothing was sent.Send Authorization: Bearer <key>.
401 invalid authorization formatThe header was there and not in the expected shape.Check for a missing Bearer , a stray newline, or a shell variable that did not expand.
401 invalid api keyThe key does not resolve. Unknown, mistyped, shorter than eight characters, or the hash does not match.Reissue it. This is deliberately the same answer for never existed and wrong, so the endpoint cannot be used to test which keys are real.
401 api key revokedSomebody revoked it. Revocation is immediate — every authentication path checks the flag.Find out who and why before reissuing. See API tokens.
401 api key expiredYou set an expiry when you created it and it has passed.Issue a replacement, cut over, then revoke the old one. In that order.
403 credential carries no subjectThe key authenticated and carries no user, so there is no role to check against.Reissue the key from an account that exists. This is a malformed-credential state, not a permission one.
403 token carries no organizationThe route needs to know which tenant, and the credential does not say.See the multi-organisation section below — this one has a specific and unobvious cause.
403 insufficient permissionsThe role of the account that created the key does not hold the permission the route requires.Grant it, or reissue the key from an account whose role does. The bulk of this article.
403 insufficient api key scopeThe route you called needs a scope the key was not issued with.Check the scopes recorded on the key at issue time. See below — this used to be driven by a request header; it is not any more.
500 permission check failedThe check could not be evaluated. A database problem on our side.Not yours. Retry, and report it if it persists.

The usual cause: the key has the creator's role, not the scopes you gave it

The single most important sentence about API keys in this platform: a key carries exactly the permissions of the user who created it. Not the scopes you typed when you created it. Not an administrator's permissions because it is a machine credential. The role on the creating account's membership, resolved fresh on every request.

So a 403 insufficient permissions from an API key is answered by asking a different question than you probably are. Not what scopes does this key have but which account created it, and what does that account's role hold?

  1. Find the key

    Identity → API Keys lists every key issued against the organisation, whoever created it, with its prefix — the first eight characters of the key itself — so you can match the one your integration is sending. Reading that list needs api_keys.read. Your own keys are visible without it, but that view shows only yours and is not an inventory.

  2. Find the account named on it, and the role on that account's membership

    That role is the key's entire authority. Note that permissions are not additive: a membership carries exactly one role, so if the account needs two roles' worth of access you build a third role containing both.

  3. Work out which permission the route needs

    The refusal deliberately does not tell you — a message naming the missing permission would describe your role structure to an attacker. The permission model has the resource strings by module; a permission is a resource, a full stop and an action.

  4. Grant it on the role, or move the key

    Granting it widens what every holder of that role can do, including the people holding it. If the integration needs something the humans should not have, create a dedicated account with a narrow role and reissue the key as that account.

`insufficient api key scope` — this changed, and the old advice about it is now wrong

Every key must now be issued with at least one scope, written as resource.action (for example users.read) or a resource.* wildcard, checked at creation time against the platform's real permission vocabulary — an unrecognised scope, a bare wildcard, or anything with no full stop in it is refused when you try to create the key, not silently accepted. Which scope a given route needs is now a fixed property of the route itself, not something a caller can name or omit. A route that has not been set up to declare a required scope refuses API-key use entirely rather than letting one through unchecked.

So the practical question this error is asking is genuinely which scope does the key have, not which role its creator holds. Check the scopes recorded on the key (Identity → API Keys) against what the route needs — fetch the platform's current list of valid scopes and wildcards from the API if you are not sure of the exact string, since it is the source of truth and can move ahead of any list written down here. Re-issuing the key with the missing scope added is the fix; there is no header, setting or workaround that changes what a route requires.

`token carries no organization`

Some routes resolve your tenant from the credential rather than from the URL. If the credential does not carry one, the request is refused rather than guessing — and there is one specific way to end up in that state that catches people out.

There is a related fail-closed behaviour worth knowing if you are writing a client: an organisation-scoped route called with an empty organisation segment answers 403 insufficient permissions rather than falling back to something permissive. A malformed URL therefore looks exactly like a missing permission. Check the path before you check the role.

The six operations an API key can never perform

Some administrative actions require a fresh second factor, proved within the last fifteen minutes. An API key has no second factor by definition, and the step-up endpoint refuses one outright:

What an API key gets if it tries to elevate
403 {"error":"this credential cannot complete step-up authentication"}

So a script cannot create a user, create or edit a role, or run an organisation lifecycle action, and — unless it is coming from a device and network the platform recognises, which a server usually is not — cannot mint an API key, change a membership, or configure single sign-on either.

The refusal for these arrives as 401 with "code":"step_up_required", not as a 403. If your client sees a 401 and runs its generic refresh-then-retry, the refresh will succeed — the credential is perfectly valid — the retry will fail identically, and the operation will silently not have happened. Branch on code, never on the status alone. Step-up authentication has the detail.

One refusal that is about your role's name, not its contents

Adding or changing an organisation member, editing a role, and configuring single sign-on each carry an extra check: the caller's role must be named exactly admin or delegated_admin. It is a comparison of the name, not of what the role holds — a role called Access reviewer holding members.write is still refused:

The name-based refusal
403 {"error":"action requires elevated delegated role"}

Matching is case-sensitive. Delegated_Admin matches nothing. See Delegated administration.

One last thing, if the key looks right and is not recognised

The platform works out whether a bearer credential is a session token or an API key with a simple test: a JSON web token contains full stops between its segments, and an API key is 64 characters of hexadecimal with none.

The practical consequence is narrow but confusing when you hit it: a credential with a full stop in it is treated as a session token, not as a key, and fails with a token error rather than a key error. If you have wrapped, prefixed or annotated the key in any way — a trailing environment marker, a version suffix — send the bare 64 characters instead.

Was this article wrong?

If a procedure here does not match what you see, or a limit we described has changed, tell us and we will fix the page. Email us about this article, or see how to get help if you need an answer rather than a correction.

Everything in troubleshooting