---
title: Errors
description: Both refusal shapes, what each status code means, which refusals are worth retrying, and why a validation failure tells you so little.
---

A refusal is JSON. There are two shapes.

Most carry only a message, which is for a log rather than for a person:

```json
{ "error": "Invalid request" }
```

A domain refusal carries a machine-readable reason as well:

```json
{
  "code": "conflict",
  "error": "Settings changed; reload before saving",
  "reason": "settings_stale"
}
```

**Branch on the status first, then on `code` and `reason` when they are there — never on `error`.** A refusal of the first shape carries neither, so code that reads `reason` unconditionally breaks on one. The message is prose and may be reworded; `code` is one of `invalid_input`, `not_found`, `conflict` and `forbidden`, and `reason` is a stable slug. Some refusals carry a `detail` object with more about what failed.

## What each status means

| Status | Means | Retry? |
| --- | --- | --- |
| `400` | The body, query or a parameter was refused | No, until you change the request |
| `401` | The token is missing, invalid, expired or revoked | Yes, once — refresh and send it again |
| `403` | The module is off, or your scope or role does not permit the action | No |
| `404` | No such record, or none you may read | No |
| `409` | A revision conflict, a reused idempotency key, or a state that forbids the operation | Only after reading again and changing the request |
| `413` | The body exceeded the limit | No |
| `500` | An unexpected failure | Yes, with backoff |

`403` does not distinguish "your role cannot" from "the module is off" in the status alone — read `reason`. `external_api_disabled` means the workspace's `api` module is off; `permission_required` means the scope or the role is the problem.

A `404` and a `403` are sometimes the same underlying situation seen from different angles: a record you may not read answers `404` rather than confirming it exists.

## Why validation failures say so little

A refused body answers `400 {"error":"Invalid request"}` without naming the field. That is deliberate: the API does not echo your request back, and it does not list the internal validation that ran. Check the request against the reference's schema for that endpoint.

## Retrying safely

An endpoint that takes an `Idempotency-Key` is safe to retry after a timeout or a `500`: reuse the **same** key and the server either does the work or replays the answer it already gave. Generating a fresh key for a retry is how a duplicate is created.

For an endpoint that takes no key — the pricing previews, the download link, `POST /api/v1/bootstrap` — a retry repeats the request. None of the four creates anything on a second call, but nothing stops the work happening twice either.
