# Errors

Every error, on every endpoint, has the same shape:

```json
{
  "error": {
    "code": "insufficient_scope",
    "message": "Missing required scope(s): calendar:write.",
    "details": { "required": ["calendar:write"], "missing": ["calendar:write"] },
    "request_id": "3f2b1c9e-8a4d-4b21-9f77-2c6e1a5b0d33"
  }
}
```

Branch on **`code`**, never on `message`. Codes are part of the contract;
messages are written for humans and may be reworded.

## `request_id`

Every response carries one, and it is also written to our logs. Quote it when
you contact support and we can pull up the exact request — including, if the
workspace has debug capture enabled, the payload we received. It saves a round
of "can you reproduce it?" every time.

## Status codes

| Status | Meaning |
| --- | --- |
| `400` | The request was malformed or failed validation |
| `401` | Authentication failed — key missing, invalid, revoked or expired |
| `403` | Authenticated, but not allowed: scope, IP/origin, or revoked grant |
| `404` | No such resource — or it is outside the properties you were granted |
| `409` | Idempotency conflict, or a block over nights a stay already occupies |
| `429` | [Rate limited](/rate-limits) |
| `5xx` | Something broke on our side. Retry with backoff |

## Codes

### Authentication (`401`)

| Code | Meaning |
| --- | --- |
| `missing_credentials` | No `Authorization` or `X-Api-Key` header |
| `invalid_credentials` | The credential is not recognised |
| `key_revoked` | The workspace revoked this key |
| `key_expired` | The key passed its expiry date |
| `token_expired` | The OAuth access token expired — [refresh it](/oauth) and retry |
| `token_revoked` | The OAuth access token was revoked; do not retry |

### Authorisation (`403`)

| Code | Meaning |
| --- | --- |
| `insufficient_scope` | Your effective scopes do not include what this endpoint needs |
| `grant_revoked` | The workspace withdrew your access |
| `integration_suspended` | Noé suspended your integration |
| `integration_not_approved` | Your integration is not approved for API access |
| `app_mismatch` | `X-Noe-App` does not match the key you authenticated with |
| `ip_not_allowed` | Your address is not on the key's allowlist |
| `origin_not_allowed` | Your browser origin is not on the key's allowlist |
| `tenant_suspended` | The workspace itself is suspended |

### Requests (`400` / `404` / `409`)

| Code | Meaning |
| --- | --- |
| `validation_failed` | Payload or query parameters were rejected. See `details.fields` |
| `not_found` | No such resource, or outside your property scope |
| `idempotency_key_required` | A write arrived without `Idempotency-Key` |
| `idempotency_key_reused` | That key was already used for a different payload |
| `idempotency_in_progress` | An identical request is still being processed |
| `stay_conflict` | A block would cover nights a stay already occupies. See `details.conflicts` |

### Stay conflicts (`409`)

A block cannot be created over — or moved onto — nights that a reservation or
a long stay already occupies, on the property itself or on a property it shares
availability with. Nothing is written. `details.conflicts` lists what is in the
way, without any guest information:

```json
{
  "error": {
    "code": "stay_conflict",
    "message": "These dates are already occupied by a stay on this property or a property it shares availability with.",
    "details": {
      "conflicts": [
        {
          "kind": "reservation",
          "property_id": "0f8b1a2c-4d5e-4f60-9a71-2b3c4d5e6f70",
          "reservation_id": "5a1d2e3f-4b5c-4d6e-8f70-1a2b3c4d5e6f",
          "check_in": "2026-11-10",
          "check_out": "2026-11-13",
          "status": "confirmed"
        }
      ]
    },
    "request_id": "…"
  }
}
```

`kind` is `reservation` or `mid_term`; `check_out` is the departure day. For a
property outside your grant, `property_id` and `reservation_id` are `null`.
Shrinking a block, or changing only its reason, is never refused.

## Which errors to retry

**Retry** `429` (honouring `Retry-After`), `5xx`, and network timeouts —
timeouts are safe precisely because writes are [idempotent](/idempotency).

**Do not retry** `400`, `401`, `403` or `404`. Nothing about repeating the same
request changes the outcome; surface these so a human can fix the cause.

`409 idempotency_in_progress` is the exception: retry it, after a short pause.
Do not retry `409 stay_conflict`: the nights stay occupied until that stay
moves or is cancelled.

## Validation details

A `400` tells you which field was wrong rather than making you guess:

```json
{
  "error": {
    "code": "validation_failed",
    "message": "The request payload failed validation.",
    "details": { "fields": ["start_date must be a calendar date in YYYY-MM-DD form"] },
    "request_id": "…"
  }
}
```

## Batch responses

Batch endpoints return `207 Multi-Status` with a per-item result, because one
bad item should not discard ninety-nine good ones. **Inspect every entry** — the
HTTP status alone does not tell you whether anything failed.

```json
{
  "results": [
    { "index": 0, "status": 201, "data": { "id": "…", "object": "block" } },
    {
      "index": 1,
      "status": 404,
      "error": { "code": "not_found", "message": "No property found with id …" }
    }
  ]
}
```
