> ## Documentation Index
> Fetch the complete documentation index at: https://docs.userkit.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Errors

> One envelope, stable codes, and a message meant for a person.

Every error in the API answers the same envelope:

```json theme={null}
{
  "error": {
    "code": "last_owner",
    "message": "the organization must keep at least one owner"
  }
}
```

<ResponseField name="code" type="string">
  Stable. It is the contract — branch on this.
</ResponseField>

<ResponseField name="message" type="string">
  For a person. It may change wording between releases, and it may be in a language
  you did not ask for. Never parse it.
</ResponseField>

Map codes to your own copy, and fall back to `message` for a code you do not
recognize — that way a new code degrades to something readable instead of a blank
dialog.

## Codes by status

### 400 — the request is wrong

| Code                    | Meaning                                                                                              |
| ----------------------- | ---------------------------------------------------------------------------------------------------- |
| `invalid_request`       | Malformed body, missing field, or a parameter that is not valid                                      |
| `weak_password`         | Under 8 characters, or missing a lowercase letter, an uppercase letter or a digit                    |
| `breached_password`     | The password appears in a public breach corpus. Checked by k-anonymity — it never leaves our process |
| `invalid_token`         | A one-time token that is invalid, expired or already used                                            |
| `unknown_permission`    | A permission outside the catalogue                                                                   |
| `confirmation_mismatch` | The typed organization name does not match                                                           |
| `unsupported_type`      | Upload content type is not JPEG, PNG, WebP or GIF                                                    |
| `invalid_size`          | Upload `size_bytes` is 0, negative or over 5 MiB                                                     |

### 401 — no valid credential

| Code                    | Meaning                                                                                                                                                                                                 |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `unauthorized`          | Missing, malformed, expired or revoked credential; also "not a member of that organization"                                                                                                             |
| `invalid_credentials`   | Wrong email or password — and the password re-check on a step-up action, which answers here rather than `403`: the caller is authenticated, it is the credential they just typed that did not check out |
| `invalid_challenge`     | Two-factor challenge invalid, expired, already spent, out of attempts, or from another environment                                                                                                      |
| `invalid_code`          | Wrong TOTP, recovery or email code                                                                                                                                                                      |
| `invalid_identity_hash` | The HMAC does not verify for this `external_id`                                                                                                                                                         |
| `invalid_grant`         | A social sign-in whose state is unknown, expired or already spent — or whose code the provider refused                                                                                                  |

### 403 — authenticated, not allowed

| Code                    | Meaning                                                                                                                                                |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `forbidden`             | Your role does not carry the permission — or you are trying to change or remove yourself                                                               |
| `role_exceeds_your_own` | The role you are handing out carries permissions your own role does not have                                                                           |
| `owner_locked`          | The owner role must keep `roles:manage`                                                                                                                |
| `system_role`           | System roles cannot be renamed                                                                                                                         |
| `origin_not_allowed`    | This publishable key does not accept calls from this origin                                                                                            |
| `unverified_session`    | This contact session claims an identity that was never proven. Anonymous sessions are not affected — see [federated mode](/en/customer-auth/federated) |
| `waitlisted`            | This contact is still in line and cannot hold a session yet — see [waitlist](/en/customer-auth/waitlist)                                               |

### 404 — not here

| Code               | Meaning                                                          |
| ------------------ | ---------------------------------------------------------------- |
| `not_found`        | Does not exist, or exists in another organization or environment |
| `unknown_provider` | This social provider is not supported                            |

<Note>
  `404` is also how cross-tenant and cross-environment reads answer. It never reveals
  that the resource exists somewhere else — the id you hold is simply not addressable
  with the credential you hold.
</Note>

### 409 — the state says no

| Code                            | Meaning                                                    |
| ------------------------------- | ---------------------------------------------------------- |
| `email_taken`                   | A user with this email already exists                      |
| `already_member`                | This user is already a member                              |
| `invite_exists`                 | A pending invitation for this email already exists         |
| `last_owner`                    | The organization must keep at least one owner              |
| `role_exists`                   | A role with this name already exists                       |
| `role_in_use`                   | Move its members to another role first                     |
| `already_enabled`               | Two-factor is already on — disable it first                |
| `waitlist_closed`               | This environment is not holding a waitlist                 |
| `setup_required`                | Start the two-factor setup first                           |
| `two_factor_disabled`           | Enable two-factor before regenerating recovery codes       |
| `environment_not_hosted`        | This environment is federated; identify via `/v1/boot`     |
| `environment_not_federated`     | This environment is hosted; switch it to federated         |
| `oauth_provider_not_configured` | This environment has no OAuth application for the provider |

### 422 — permanently failed

| Code                     | Meaning                                                        |
| ------------------------ | -------------------------------------------------------------- |
| `job_failed_permanently` | Internal job callback: ack the message, a retry cannot succeed |

### 429 — too many

| Code           | Meaning                                                        |
| -------------- | -------------------------------------------------------------- |
| `rate_limited` | Too many requests. `Retry-After` carries the window in seconds |

See [Rate limits](/en/api-reference/rate-limits).

### 5xx — ours

| Code                   | Status | Meaning                                      |
| ---------------------- | ------ | -------------------------------------------- |
| `internal_error`       | 500    | Something failed on our side. Safe to retry. |
| `job_failed`           | 500    | Internal job callback: retry the message     |
| `provider_unavailable` | 502    | A social provider could not be reached       |

### 501 — the server cannot do this

| Code                             | Meaning                                          |
| -------------------------------- | ------------------------------------------------ |
| `two_factor_unavailable`         | Two-factor is unavailable on the server          |
| `uploads_unavailable`            | File storage is unavailable on the server        |
| `federated_identity_unavailable` | Federated identity is unavailable on the server  |
| `jwt_unavailable`                | Session JWT minting is unavailable on the server |
| `social_login_unavailable`       | Social sign-in is unavailable on the server      |

These are platform states, not user errors. `GET /v1/session` reports
`two_factor_available` and `uploads_available` so a client can hide the feature
instead of discovering it here.

## Handling them

```ts theme={null}
const MESSAGES: Record<string, string> = {
  last_owner: "The organization needs at least one owner.",
  role_in_use: "Move this role's members somewhere else first.",
  origin_not_allowed: "This domain is not on the key's allowed list.",
};

async function call(path: string, init?: RequestInit) {
  const response = await fetch(path, init);
  if (response.ok) return response.status === 204 ? null : response.json();

  const { error } = await response.json();
  // An unknown code degrades to the API's own message rather than to nothing.
  throw new Error(MESSAGES[error.code] ?? error.message);
}
```

## Two things that are not errors

**A pagination value out of range** falls back to the default rather than answering
`400`. Do not rely on a `400` to catch a bad page size.

**`202` on the non-enumerable endpoints** is a success. It does not mean "queued"
and it does not mean the address exists — it means the answer would be the same
either way.
