---
title: Conventions
description: Idempotency keys, cursor pagination, optimistic revisions and request limits, described as the server actually behaves rather than as guarantees.
---

Every endpoint shares these. What follows is what the server does, not a guarantee invented for the documentation — where a guarantee is narrower than you might assume, that is said.

## Idempotency

Most mutations require an `Idempotency-Key` header, 8 to 128 characters, and refuse a request without one with `400`. The reference names it on each endpoint that takes it — four `POST` endpoints do not: the two pricing previews, which write nothing, the download link, and `POST /api/v1/bootstrap`, which installs standard objects and is safe to repeat because an object already installed is left as it is.

Send the same key with the same body and you get the first answer back, replayed, without the work happening twice. Send the same key with a **different** body and the request is refused with `409` and the reason `idempotency_key_reused`.

Two limits on that guarantee are worth knowing.

**A receipt is scoped to you and to the action**, not globally. It is keyed on the user, the action and the key together, so the same key used by a different person, or by you against a different endpoint, is a different command. A key only has to be unique within what you yourself are doing.

**A replay answers the body that was stored.** If the shape of an endpoint's response changes in a release, a key issued before it replays the older body. That is inherent to replaying a stored response, and it is why a key belongs to one request rather than being reused across a session.

## Pagination

Two kinds of list exist, and they page differently.

Most lists — products, price books, quotes — take `limit` and `after`, and answer `nextCursor`. Pass `nextCursor` back as `after` for the next page, and stop when it is `null`.

Record lists take more: `limit`, `dir`, `orderBy` or `view`, `relatedTo` with `via`, and `cursor` or `after`. Four combinations are refused with `400`, and they cannot be expressed in a parameter list:

- `via` requires `relatedTo`
- `orderBy` cannot be combined with `view`
- `cursor` requires one of `orderBy` or `view`
- `after` cannot be combined with either — page an ordered query with `cursor`

**A cursor belongs to the ordering that produced it.** The ordering — the sort, the direction, the related-record filter, the view — travels inside the cursor. Replay a cursor against a changed view and the request fails loudly with `cursor_ordering_mismatch` rather than answering a page that looks right and is not. Fetch from the first page again when the ordering changes.

**`total` is counted on the first page only.** A request with no `after` and no `cursor` answers a count; every later page answers `total: null`. The count is paid once rather than on every page, so hold the first page's figure if you want to show one.

## Revisions

Records, objects, fields, saved reports and the commercial policy carry a `revision`. An update sends the revision it was read at, and a stale one is refused with `409`.

This is optimistic concurrency rather than a lock: nothing is held while you think. Read, send the revision you read, and handle the refusal by reading again.

## Request limits

A request body is limited to **1 MB**, except a note image upload, which is **4 MB**. The limit is applied before the route runs, so an oversized request is refused with `413` whatever endpoint it was aimed at.

## Headers

| Header                           | When                      |
| -------------------------------- | ------------------------- |
| `Authorization: Bearer <token>`  | Every request to this API |
| `Idempotency-Key`                | Every mutation            |
| `Content-Type: application/json` | Every request with a body |

`X-CRM-CSRF` is not yours to send. The web application uses it for its own cookie-session writes; a bearer-token request does not take that path and does not need it. Credentialed CORS likewise allows the application's own origin only — it does not apply to a server-to-server call, which sends no browser origin.

## Dates and money

Timestamps are ISO 8601 strings in UTC. Date-only fields — a purchase date, a schedule's start and end — are `YYYY-MM-DD`.

**Money is a decimal string, never a number.** Totals, unit prices, tax and discounts arrive as strings like `"1234.56"` so no value passes through a binary float. Parse them with a decimal library, and do not add them as numbers.
