# Errors, idempotency and pagination
> Source: /guides/errors-idempotency-pagination
> The conventions to build against before you scale up a Partner API integration.

These three conventions are consistent across the whole Partner API. Getting
them right once, in a shared client, is far less work than handling them per
endpoint.

## Errors [#errors]

Every error uses the same envelope:

```json
{
  "type": "validation_error",
  "timestamp": "2026-10-09T18:22:41.218Z",
  "code": "invalid_parameter",
  "message": "Human-readable summary.",
  "details": {},
  "errors": []
}
```

`type`, `timestamp`, `code` and `message` are always present. `details` and
`errors[]` are optional.

<Warning>
  **Branch on `code`, never on `message`.** Messages are written for humans and
  can change. In production, 5xx messages are deliberately masked, so there is
  nothing useful to parse there either.
</Warning>

### What the statuses mean [#what-the-statuses-mean]

| Status | Meaning |
|---|---|
| 400 | Validation failed, or an `Idempotency-Key` longer than 255 characters |
| 401 | Missing, invalid, expired or revoked credential |
| 403 | Authenticated, but missing a required scope — or the wrong key level |
| 404 | Not found, **or outside your organization or venue reach** |
| 409 | Idempotency conflict (see below) |
| 413 | Upload above 10 MB |
| 415 | Unsupported upload type |
| 429 | Rate limited — back off and retry |

### 404 is doing two jobs [#404-is-doing-two-jobs]

Out-of-scope resources return **404, not 403**. This is deliberate: a caller
cannot use the error code to discover whether a reservation exists at a
property they cannot reach.

The practical consequence is that a 404 does not prove something was deleted.
If you are reconciling, treat 404 as "not visible to me" rather than "gone".

The one exception: passing `propertyIds` outside your scope on the reservations
list returns **403** rather than filtering silently.

## Idempotency [#idempotency]

<ParamField header="Idempotency-Key" type="string">
  Optional. Supported on `POST`, `PUT`, `PATCH` and `DELETE` under `/partner`.
  Maximum 255 characters.
</ParamField>

Send one on every write. On a replay you get the stored response back, with:

```http
X-Idempotent-Replay: true
```

### What counts as "the same request" [#what-counts-as-the-same-request]

Method, path, sorted query string and canonical body. Two details matter:

- **JSON key order is ignored** — reserialising your body will not break a
  replay.
- **Array order matters** — `[a, b]` and `[b, a]` are different requests. If
  you build arrays from a set or a map, sort them before sending, or your retry
  will be rejected as a conflict.

### Conflicts [#conflicts]

You get a **409** when:

- the same key is reused with a *different* request, or
- the same key is sent while the first request is still in flight.

An in-flight request holds its lease for a few minutes. If a request times out
on your side, retrying with the same key shortly afterwards may legitimately
return 409 — wait and retry rather than generating a new key, which would risk
performing the operation twice.

Keys are scoped to your tenant, so they cannot collide with another partner's.

<Note>
  The `Idempotency-Key` header is not currently declared in the OpenAPI
  document, so you will not see it in the generated reference. It is supported
  regardless, as described here.
</Note>

### Already-idempotent operations [#already-idempotent-operations]

Some endpoints are naturally safe to repeat: acknowledging an arrival, and
adding codes to a pool (duplicates are reported rather than failing). Send a
key anyway for the replay semantics.

## Pagination [#pagination]

Two styles, depending on the endpoint.

### Offset [#offset]

The default: `limit` (1–100) with `offset`. Fine for small, bounded lists like
venues or programs.

### Keyset cursor [#keyset-cursor]

`GET /v1/partner/reservations` supports a cursor, and you should use it for
anything large:

- Send `cursor`, read `nextCursor`, repeat until `nextCursor` is null.
- Ordered by check-in date, then external reservation id.
- A cursor **overrides `offset`**; the response echoes `offset: 0`.

<Warning>
  **A malformed cursor is ignored, not rejected.** The request succeeds and
  returns the first page. A paging loop that mangles its cursor will therefore
  run forever over page one rather than erroring. Assert that each page
  advances, and cap the number of iterations.
</Warning>

## Rate limits and backoff [#rate-limits-and-backoff]

Limits exist at both the per-key and edge layers, and a few individual routes
carry their own — custom-domain recheck and retry, and the public guest-invite
send.

Rather than coding to specific numbers, which can change:

- treat **429 as a normal condition** under load, not an error to alert on;
- retry with **exponential backoff and jitter**;
- keep a **concurrency ceiling** in your client, so a burst of work does not
  turn into a burst of requests;
- remember that most data changes on a multi-hour cycle, so polling faster
  rarely gets you anything. See
  [how the data flows](/guides/reservation-and-guest-data-flow).
