# Error codes (/docs/reference/errors)



Every error response is JSON with the same outer shape:

```json
{ "error": { "code": "name_taken", "message": "a branch with this name exists" } }
```

`code` is stable and snake_case; match on it. `message` is for people and can change. A few codes add keys inside `error` (listed below). The HTTP status says the class of failure; the code says which one.

The data API adds `detail`, `hint` and `sqlstate` to errors that come from Postgres. See [Data API errors](#data-api-errors).

## Management, login and gateway codes [#management-login-and-gateway-codes]

These come from the management API, the login API (the dashboard sign-in and app users) and the gateway. Where a code has two statuses, each row says when.

### Request and authentication [#request-and-authentication]

| Status | Code                 | Meaning                                                                                                                                                  |
| ------ | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 400    | `invalid_request`    | A path id isn't a valid UUID, the body isn't well-formed JSON, or the query string is malformed.                                                         |
| 415    | `invalid_request`    | The request isn't `Content-Type: application/json`.                                                                                                      |
| 422    | `invalid_request`    | The body is JSON but has the wrong fields, a field is missing or out of range, or a name is invalid.                                                     |
| 401    | `unauthorized`       | No credential, or a bad token or key.                                                                                                                    |
| 409    | `tenant_required`    | Sign-in (`/auth/v1/sign-in`) for a member of several tenants with no `?tenant=`. Sent only after correct credentials; `error.tenants` lists the choices. |
| 403    | `forbidden`          | The `tenant_id` in the body isn't the caller's tenant.                                                                                                   |
| 404    | `not_found`          | The resource doesn't exist, or belongs to another tenant (both answer the same). The gateway also answers `not_found` for an unknown host or route.      |
| 405    | `method_not_allowed` | The gateway has no handler for this method on this path; the `Allow` header lists the methods it has.                                                    |
| 413    | `too_large`          | The body is over the limit (2 MB on the management API, 8 KiB on the dashboard login endpoints, 64 KiB on the app-user endpoints).                       |
| 429    | `rate_limited`       | Too many attempts on a login endpoint.                                                                                                                   |
| 500    | `internal`           | Unexpected; details are only in the server logs.                                                                                                         |
| 502    | `unavailable`        | A service behind the API answered something unexpected, or failed.                                                                                       |
| 503    | `unavailable`        | A service behind the API (the compute, the user service, the identity service) can't be reached right now.                                               |
| 504    | `timeout`            | A service didn't answer in time. The gateway also answers `timeout` with 408 when a request times out.                                                   |

### Members, access and invites [#members-access-and-invites]

Any project route can answer the first four rows. See [Access per route](/docs/reference/access) for which level each route needs.

| Status | Code                             | Meaning                                                                                                                                                                                                                                                                                                                                                                                                           |
| ------ | -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 403    | `forbidden`                      | The caller's grant is too low, or the Postgres role asked for isn't in their effective set. `error.required` names what is needed: a tenant, project or branch level (`tenant admin`, `developer`, `admin`), `instance admin`, `owner`, `role:<name>`, or `connect` (the member's database role may not connect to that Postgres database). A project the caller has no grant on answers 404 `not_found` instead. |
| 403    | `no_access_yet`                  | The caller is a member with no grants anywhere. A tenant admin must grant them a project.                                                                                                                                                                                                                                                                                                                         |
| 403    | `pending_approval`               | The caller signed up and waits for a tenant admin to approve them.                                                                                                                                                                                                                                                                                                                                                |
| 403    | `member_suspended`               | A tenant admin suspended the caller's membership. Every route refuses them except `GET /v1/me`; a tenant admin can reinstate them.                                                                                                                                                                                                                                                                                |
| 403    | `member_offboarded`              | The caller was offboarded: removed or declined, which ends their sessions and bans their account. Every route refuses them except `GET /v1/me`, until they accept a new invite.                                                                                                                                                                                                                                   |
| 409    | `personal_login_exists`          | The caller already has a live personal login on this database. Reset its password, or delete it and create a new one to change the name.                                                                                                                                                                                                                                                                          |
| 403    | `not_available_in_platform_mode` | The feature (or self-sign-up) isn't designed for platform mode yet.                                                                                                                                                                                                                                                                                                                                               |
| 403    | `sign_up_closed`                 | Open sign-up is off and the tenant already has members.                                                                                                                                                                                                                                                                                                                                                           |
| 403    | `setup_code_invalid`             | The instance has no member yet and its first sign-up needs the setup code that `chelabase-init secrets` printed; it was missing or wrong. No account is created.                                                                                                                                                                                                                                                  |
| 409    | `last_tenant_admin`              | The change would leave the tenant without an active tenant admin (demoting or removing the last one).                                                                                                                                                                                                                                                                                                             |
| 409    | `link_not_allowed`               | A link invite (`delivery: "link"`) for someone who already has a password or a verified email, or whom another tenant holds. Send it by email instead; `error.email_available` says whether email delivery works now.                                                                                                                                                                                             |
| 409    | `signup_pending`                 | The person signed up and waits for approval. Approve or decline the sign-up instead of inviting them; `error.member_id` names the member.                                                                                                                                                                                                                                                                         |
| 409    | `invite_closed`                  | The invite was revoked, accepted or replaced since. Reload the list.                                                                                                                                                                                                                                                                                                                                              |
| 410    | `invite_expired`                 | The invite's code has expired. Ask for a new invite.                                                                                                                                                                                                                                                                                                                                                              |
| 410    | `invite_revoked`                 | The invite was revoked.                                                                                                                                                                                                                                                                                                                                                                                           |
| 503    | `invite_incomplete`              | The code was accepted but a later step failed. `error.accept_ticket` lets the page retry without the spent code.                                                                                                                                                                                                                                                                                                  |
| 503    | `member_role_reconcile_failed`   | The caller's Postgres role on this branch couldn't be prepared right now. Comes with `Retry-After`; try again.                                                                                                                                                                                                                                                                                                    |
| 503    | `compute_tls_failed`             | Chelabase couldn't open a verified TLS connection to this branch's compute, or the compute refused the connection's address or client certificate. Comes with `Retry-After`; try again. If it persists, the operator should check the compute CA and the orchestrator's client certificate.                                                                                                                       |

Two more refusals use general codes. A grant or override below Admin that lists an owner-level, `LOGIN` or reserved role, or a role whose chain reaches one, is 422 `invalid_request`, and so is a grant at any level (Admin too) whose role chain holds `ADMIN` on another role; the message names the role, its chain and the database (`error.role`, `error.chain`, `error.database_id`), and invites and approvals also name the grant's project (`error.project_id`). A Developer who acts as such a role, or creates a data key bound to one, gets 403 `forbidden` with `required: "admin"`.

### Projects and databases [#projects-and-databases]

| Status | Code                   | Meaning                                                                                                                                                                 |
| ------ | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 409    | `name_taken`           | A database or branch with this name exists.                                                                                                                             |
| 409    | `project_deleting`     | The project is being deleted.                                                                                                                                           |
| 409    | `database_not_ready`   | The database isn't ready, or is being deleted.                                                                                                                          |
| 409    | `already_deleting`     | The database is already being deleted.                                                                                                                                  |
| 409    | `data_api_disabled`    | The Postgres database isn't served by the data API; enable it first.                                                                                                    |
| 409    | `data_api_not_enabled` | The data API is off for the Postgres database on a branch.                                                                                                              |
| 409    | `conflict`             | The change conflicts with the current state (for example the main database can't be removed from the data API, or the worker refused the change; the message says why). |
| 502    | `unavailable`          | The worker failed to apply the change.                                                                                                                                  |
| 503    | `worker_unavailable`   | The worker can't be reached.                                                                                                                                            |
| 503    | `not_applied`          | The change is stored but not applied yet; it will be retried.                                                                                                           |

### Branches [#branches]

| Status | Code                      | Meaning                                                                                                                                                                                                                                                                                                                                                       |
| ------ | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 409    | `branch_not_ready`        | The branch isn't ready, or is busy or being deleted.                                                                                                                                                                                                                                                                                                          |
| 409    | `backup_branch`           | A restore backup can't take this operation (no compute, can't be protected or rewritten).                                                                                                                                                                                                                                                                     |
| 409    | `main_branch`             | Main can't be deleted and has no parent to reset to.                                                                                                                                                                                                                                                                                                          |
| 409    | `has_children`            | The branch (or one of its backups) has child branches.                                                                                                                                                                                                                                                                                                        |
| 409    | `branch_protected`        | The branch is protected. On a branch route (reset, restore, delete): unprotect it first. From the table API, and the SQL editor's guard refusal on a protected branch (Postgres error 42501 with hint `chelabase:branch_protected`): schema changes there need an Admin; ask one, or make the change on a branch of your own and merge it.                    |
| 409    | `branch_cap_reached`      | The database has `max_branches`.                                                                                                                                                                                                                                                                                                                              |
| 409    | `credentials_unavailable` | The branch has no endpoint, or its owner credentials are missing.                                                                                                                                                                                                                                                                                             |
| 500    | `credentials_unavailable` | Stored credentials can't be opened with the current secrets key.                                                                                                                                                                                                                                                                                              |
| 409    | `owner_password_unknown`  | The owner's password was set in SQL; reset it first.                                                                                                                                                                                                                                                                                                          |
| 409    | `branch_merge_busy`       | A schema merge holds this branch as its source or target (it is rehearsing, applying, finishing, or failed half applied in its online phase), or a rehearsal's throwaway branch still hangs off it, so reset, restore and delete are refused. `error.merge_id` names the merge; `error.rehearsal_branch_id` names the throwaway branch.                       |
| 409    | `review_changed`          | The branch's review (code owned by roles other than the owner or a superuser that can run in someone else's session) differs from the hash the request carries: protecting without `review_hash` while the list isn't empty, or with a stale one, and a stale `hash` on acknowledge or fix. `error.review` carries the current review (its items and `hash`). |

### Table API [#table-api]

| Status | Code                            | Meaning                                                                                                                                                                                                                  |
| ------ | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| 403    | `permission_denied`             | Postgres refused a statement for the role you act as (SQLSTATE 42501); the same code as the data API's.                                                                                                                  |
| 409    | `has_dependents`                | A drop has dependents and the request doesn't confirm a cascade (dry run or not); nothing was changed. `error.dependents` lists what a cascade would also drop and its `hash`, to send as `cascade.dependents_hash`.     |
| 409    | `dependents_changed`            | The objects a cascade would drop changed since the dry run.                                                                                                                                                              |
| 409    | `index_build_failed`            | A concurrent index build failed after creating its index; that invalid index was dropped.                                                                                                                                |
| 409    | `lock_timeout`                  | A lock the change needs was held past `lock_timeout_ms`.                                                                                                                                                                 |
| 409    | `conflict`                      | Existing rows break a unique, foreign-key or exclusion constraint (23505, 23503, 23P01), or a deadlock or serialization failure (40P01, 40001; safe to retry).                                                           |
| 400    | `constraint_violation`          | Rows break NOT NULL or a check, or a value is out of range or can't be read as its type (23502, 23514, class 22). Not the app users' 422 `constraint_violation`, which is another endpoint's.                            |
| 400    | `schema_change_failed`          | Postgres refused a table API statement (any SQLSTATE not mapped to another code); the error carries `sqlstate`, `message`, `detail`, `hint` and `position`.                                                              |
| 409    | `privileges_not_applied`        | A privilege change ran, but the privileges read back afterwards differ from the requested end state (for example a grant Postgres skipped because the role running it couldn't give it). Nothing of the change was kept. |
| 409    | `privileges_changed_since_read` | An object's privileges, or a function's signature, changed since the privileges read the request was built from. Read them again and retry.                                                                              |
| 422    | `exposes_data_api`              | "All rows" was refused because the data API can reach the table through that role; `error.chain` shows how.                                                                                                              |

### Schema merges [#schema-merges]

Merge routes also answer the general codes above: `not_found` for an unknown merge (or another tenant's, or one of another database), `branch_not_ready` on a start, rehearse, apply and retry (a branch of the merge isn't ready), `backup_branch` and `invalid_request` on a start (a step id with `:`, `.`, a control character or `phase1` is a 422 `invalid_request` on a save), and `conflict` with 409 when the merge is in a state that can't take the request (for example editing a merge that is applying, or exporting a plan before there is one). See [Merging schema between branches](/docs/using/merging-schema).

| Status | Code                         | Meaning                                                                                                                                                                                                                                                                                                                                           |
| ------ | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 409    | `merge_open_exists`          | The branch and its parent already have an open merge for this Postgres database. Raised on start, and on recompute when a newer open merge exists. `error.merge_id` names it.                                                                                                                                                                     |
| 409    | `merge_busy`                 | Another merge is changing the target branch: applying, online, finishing, or failed half applied on it (`error.merge_id`). Raised on start, apply and retry. A start also answers it, with `Retry-After: 1`, when the pair's open merge finished a moment earlier: try again.                                                                     |
| 409    | `merge_version_conflict`     | `version` in a save, a rehearse or an apply isn't the merge's. `error.current` carries the current merge.                                                                                                                                                                                                                                         |
| 409    | `merge_not_rehearsed`        | Apply needs a `rehearsed` merge, and this one is in another state, or changed while the request ran.                                                                                                                                                                                                                                              |
| 409    | `merge_stale`                | The merge is stale: a schema changed since the merge was computed (found by the rehearsal, or by the apply's check of the target). Recompute it.                                                                                                                                                                                                  |
| 409    | `merge_conflicts_open`       | A conflict has no resolution; the message lists their ids. Raised on rehearse and apply.                                                                                                                                                                                                                                                          |
| 409    | `merge_drops_unacknowledged` | A step that loses data, gives a role `BYPASSRLS` or owner-level, or renames a role, has no acknowledgement; the message lists the items.                                                                                                                                                                                                          |
| 409    | `merge_confirm_required`     | The target is protected, and apply needs `confirm: true`.                                                                                                                                                                                                                                                                                         |
| 409    | `branch_merge_busy`          | See [Branches](#branches).                                                                                                                                                                                                                                                                                                                        |
| none   | `merge_base_unavailable`     | Not an HTTP error. It is the start of the `error` of a merge left as a `draft` with no analysis because its base can't be read (outside the history window and not a branch point the storage layer keeps, or stored by another model version): the text is `merge_base_unavailable: ` followed by the reason. Recompute with `rebaseline: true`. |

A merge that fails while running (`failed`, `stale`) shows its reason in the merge's own `error` and `failed_phase`, not as an HTTP error. The worker starts the `error` with a code where it knows the cause: `merge_stale:` (the target changed; nothing of the plan's transaction was applied), `merge_target_changed:` and `merge_target_gone:` (the plan's transaction is no longer on the target, or the target is gone: `failed` with no phase), `merge_half_applied:` (a recompute found the plan's transaction on the target: `failed` in `online`), and `merge_resolution_refused:` (a saved choice was refused: a `draft`). An apply interrupted before its transaction committed returns to `rehearsed` with an `error` that starts "interrupted before the plan's transaction committed". [Merging schema](/docs/using/merging-schema#error-texts) lists them.

### Login roles and data API keys [#login-roles-and-data-api-keys]

| Status | Code                          | Meaning                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| ------ | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| 409    | `role_exists`                 | A login role with this name exists.                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| 409    | `role_cap_reached`            | The branch has its maximum number of login roles.                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| 409    | `role_in_use`                 | Data API keys still use the role. `error.keys` lists them as `{id, name}`.                                                                                                                                                                                                                                                                                                                                                                                                                       |
| 422    | `invalid_role_name`           | The name is empty, too long (over 63 bytes), has a NUL byte, or doesn't fit the naming rules.                                                                                                                                                                                                                                                                                                                                                                                                    |
| 422    | `reserved_role_name`          | The name is reserved, or the role is the owner, which can't be deleted.                                                                                                                                                                                                                                                                                                                                                                                                                          |
| 422    | `role_attribute_not_allowed`  | An attribute the roles API doesn't take: a connection limit under -1. Other attributes outside the allowed set (LOGIN, owner-level, INHERIT, CONNECTION LIMIT, VALID UNTIL, BYPASSRLS, memberships) are refused as `invalid_request` unknown fields.                                                                                                                                                                                                                                             |
| 422    | `role_membership_not_allowed` | A membership in a role Chelabase keeps for itself (the owner, `authenticator`, `anon`, `authenticated`, `neon_superuser`, `cloud_admin`, `chelabase_…`, `pg_…`), in the role itself, in a role that doesn't exist or the owner can't administer, or in a group that is itself a member of one of those, at any depth.                                                                                                                                                                            |
| 422    | `invalid_successor`           | A role delete's `successor` can't receive the role's objects: it's the role itself, the owner, a reserved role (`pg_…`, `cb_m_…`, `chelabase_…` other than `chelabase_orphaned`, the platform's own), a personal login, owner-level (directly or through a membership), a role the owner can't `SET ROLE` to, or one that doesn't exist. Omit `successor` to hand them to `chelabase_orphaned`.                                                                                                  |
| 409    | `exposure_unconfirmed`        | A role delete to a named `successor` would leave `SECURITY DEFINER` routines or views without `security_invoker`, granted to other roles, running with the successor's rights, and `confirm` isn't the current token. `error.exposures` lists them as `{database, kind, identity, grantees}` and `error.confirm` carries the current token (also answered by `GET …/roles/{role}/delete-preview`): review them, then delete again with `?confirm=<token>`. `chelabase_orphaned` never needs one. |
| 403    | `role_refused`                | The worker refused the role (on the SQL editor and console routes).                                                                                                                                                                                                                                                                                                                                                                                                                              |
| 422    | `role_refused`                | The worker refused the role when a key is created (it doesn't exist, or it's owner-level: superuser, `CREATEROLE`, `CREATEDB`, `REPLICATION` or a `neon_superuser` member; the message says which). `BYPASSRLS` alone is allowed.                                                                                                                                                                                                                                                                |
| 500    | `role_privilege_mismatch`     | The role's privileges in the database don't match what Chelabase recorded.                                                                                                                                                                                                                                                                                                                                                                                                                       |
| 409    | `public_key_exists`           | The branch already has a public key.                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| 409    | `public_key_not_revocable`    | A public key can only be rotated.                                                                                                                                                                                                                                                                                                                                                                                                                                                                |

### App users [#app-users]

| Status   | Code                       | Meaning                                                                                                                                                              |
| -------- | -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 404      | `app_users_disabled`       | App users were never enabled for the project.                                                                                                                        |
| 409      | `app_users_disabled`       | App users aren't enabled (management API and login API).                                                                                                             |
| 409      | `app_users_has_users`      | The users table has users, so app users can't be disabled.                                                                                                           |
| 409      | `app_users_table_exists`   | The users table exists already.                                                                                                                                      |
| 409      | `app_users_location_fixed` | The users table's location can't change.                                                                                                                             |
| 409      | `email_taken`              | Another user has this email. On sign-up this happens only when email verification is off.                                                                            |
| 401      | `invalid_credentials`      | Wrong email or password, or the current password is wrong.                                                                                                           |
| 401      | `refresh_token_invalid`    | The refresh token is invalid.                                                                                                                                        |
| 401      | `refresh_token_reused`     | The refresh token was used already; the session is revoked.                                                                                                          |
| 401      | `user_token_invalid`       | No user token, or an invalid one.                                                                                                                                    |
| 403      | `email_not_verified`       | The email isn't verified.                                                                                                                                            |
| 403      | `user_inactive`            | The user is deactivated or locked.                                                                                                                                   |
| 410      | `sign_in_expired`          | Studio's sign-in (not an app-user flow): the sign-in has expired or is unknown.                                                                                      |
| 400, 422 | `invalid_code`             | The verification or reset code is wrong or expired. The dashboard login API answers 400; the app-user API answers 422.                                               |
| 400, 422 | `weak_password`            | The password doesn't meet the policy. Same split: 400 on the dashboard login API, 422 on the app-user API and the management API.                                    |
| 422      | `unknown_column`           | Sign-up `data` names a column the users table doesn't have or sign-up can't set.                                                                                     |
| 422      | `constraint_violation`     | The users table refused the row. `error.constraint` names the constraint.                                                                                            |
| 503      | `identity_unavailable`     | The identity service that holds the project's users can't be reached. Try again. When a deactivate or delete answers this, the user's sessions were already revoked. |

### SQL editor [#sql-editor]

| Status | Code                     | Meaning                                                                                                     |
| ------ | ------------------------ | ----------------------------------------------------------------------------------------------------------- |
| 409    | `sql_confirm_required`   | The guard held statements back. `error.statements` lists them; send again with `confirm: true` to run them. |
| 409    | `run_id_in_use`          | You have a run with this id in flight.                                                                      |
| 429    | `sql_run_limit`          | You have 4 runs in flight, or the Postgres database has 4. Comes with `Retry-After`.                        |
| 422    | `schema_too_large`       | The schema is over 8 MiB; use keyword completion.                                                           |
| 409    | `saved_query_name_taken` | You have a saved query with that name.                                                                      |
| 409    | `saved_query_conflict`   | `version` is stale. `error.current` carries the current row.                                                |
| 409    | `saved_query_cap`        | You already keep 500 saved queries (the cap is per user and database).                                      |

### Data API codes on the gateway [#data-api-codes-on-the-gateway]

The gateway authenticates data API requests before they reach the data API, and answers with the data API's error body. These codes come from the gateway, with the statuses below.

| Status | Code                         | Meaning                                                                                                                             |
| ------ | ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| 401    | `unauthorized`               | The key is missing, invalid or revoked, or an endpoint that needs a secret key got another kind (or a public one got a secret key). |
| 401    | `user_token_invalid`         | The `Chelabase-User` token is missing, malformed or invalid.                                                                        |
| 401    | `user_token_expired`         | The `Chelabase-User` token has expired.                                                                                             |
| 401    | `user_session_revoked`       | The user's session was revoked.                                                                                                     |
| 400    | `user_token_with_secret_key` | A `Chelabase-User` token came with a secret key.                                                                                    |
| 429    | `rate_limited`               | More than 20 lookups per second of keys the gateway doesn't know, from one IP address.                                              |

A bad user token never falls back to the key's own role.

## Data API errors [#data-api-errors]

The data API (`POST /data/v1/query`, `GET /data/v1/schema`) answers with:

```json
{
  "error": {
    "code": "conflict",
    "message": "duplicate key value violates unique constraint \"users_email_key\"",
    "detail": "Key (email)=(a@example.com) already exists.",
    "sqlstate": "23505"
  }
}
```

`detail`, `hint` and `sqlstate` are present only on a database error that Chelabase maps to a code, and carry Postgres's own text. Everything else has `code` and `message` only. A 503 and a 500 never show Postgres's text; the 500 message carries a request id and the details go to the log with it.

| Status | Code                         | Meaning                                                                                                                                                                                                                                                                                                                            |
| ------ | ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 400    | `invalid_query`              | The body isn't a valid query or doesn't plan: syntax error, bad filter, wrong types, empty `select` or `returning`, an update or delete with neither `where` nor `all`.                                                                                                                                                            |
| 400    | `unknown_column`             | A column that doesn't exist or that the key's role can't reach.                                                                                                                                                                                                                                                                    |
| 400    | `invalid_value`              | Postgres rejected a value: not-null (23502) or check (23514) violation, or any type or value error (class 22).                                                                                                                                                                                                                     |
| 400    | `raised`                     | A function raised an exception (`RAISE EXCEPTION`, P0001).                                                                                                                                                                                                                                                                         |
| 400    | `user_token_with_secret_key` | A `Chelabase-User` token came with a secret key.                                                                                                                                                                                                                                                                                   |
| 401    | `unauthorized`               | The key is missing, unknown or revoked, or its branch is gone.                                                                                                                                                                                                                                                                     |
| 401    | `user_token_invalid`         | The `Chelabase-User` token is invalid.                                                                                                                                                                                                                                                                                             |
| 401    | `user_token_expired`         | The `Chelabase-User` token has expired.                                                                                                                                                                                                                                                                                            |
| 401    | `user_session_revoked`       | The user's session was revoked.                                                                                                                                                                                                                                                                                                    |
| 403    | `permission_denied`          | Postgres refused the role (42501); the same code as the table API's.                                                                                                                                                                                                                                                               |
| 403    | `role_not_allowed`           | A secret key's role is owner-level or has system privileges: superuser, `CREATEROLE`, `CREATEDB`, `REPLICATION`, or a member (at any depth) of a superuser role, `neon_superuser`, `chelabase_admin` or the `pg_*_server_*` roles: what key creation refuses. Take those away, or use another role (`BYPASSRLS` alone is allowed). |
| 404    | `unknown_table`              | A table or view that doesn't exist or that the key's role can't reach.                                                                                                                                                                                                                                                             |
| 404    | `unknown_function`           | A function that doesn't exist, can't be called through the API, or that the key's role can't execute.                                                                                                                                                                                                                              |
| 404    | `unknown_database`           | The database doesn't exist, isn't served, or isn't on the key's allow-list. One text for all three, so a key can't discover database names.                                                                                                                                                                                        |
| 404    | `unknown_role`               | Studio's console only: the chosen role doesn't exist (any more).                                                                                                                                                                                                                                                                   |
| 409    | `conflict`                   | A unique (23505), foreign-key (23503) or exclusion (23P01) violation.                                                                                                                                                                                                                                                              |
| 409    | `retry`                      | A serialization failure or deadlock (40001, 40P01). Safe to run again.                                                                                                                                                                                                                                                             |
| 413    | `too_large`                  | The body is over 1 MiB, or the response is over the response limit (`CHELABASE_REST_MAX_RESPONSE_BYTES`, default 16 MiB).                                                                                                                                                                                                          |
| 429    | `rate_limited`               | See the gateway table above.                                                                                                                                                                                                                                                                                                       |
| 500    | `internal`                   | Unexpected. The message includes a request id; check the logs.                                                                                                                                                                                                                                                                     |
| 503    | `unavailable`                | The database can't be reached right now. See below.                                                                                                                                                                                                                                                                                |
| 504    | `timeout`                    | A statement ran past its timeout (57014), or the transaction timeout passed (25P04). Both follow `statement_timeout_ms`.                                                                                                                                                                                                           |

On a data API route the gateway also answers an unknown path (404) or a wrong method (405) with `invalid_query`, and the 405 carries `Allow`.

### 503 and Retry-After [#503-and-retry-after]

Every 503 from the data API carries `Retry-After`: `1` when the gateway answers it, `2` when the data API service does. Causes:

* the branch's database can't be reached, refuses the connection, or is out of connections or resources (Postgres classes 08, 28 and 53, and 57P01 to 57P03);
* no connection to the branch in time, or too many connections in use;
* the service can't look up the branch, or the branch isn't ready;
* the key's role can't be used right now (dropped, or its grant is gone until the bootstrap runs again);
* the data API's schema is missing; it will be reinstalled;
* the gateway can't check the key or the user's session.

The 503 message is fixed text.

<Callout type="info">
  A response that didn't come from the data API (a proxy error page) has no error body from it. The gateway then builds one from the failure: 502 is `unavailable`, 504 and 408 are `timeout`, 400 is `invalid_request`, 413 is `too_large`, anything else is `internal`.
</Callout>

For how to send queries and handle these errors, see the [Data API guide](/docs/using/data-api).
