# Pagination, filtering, and conditional requests

## Envelope

Single resource:

```json
{ "data": { "object": "product", "id": "prd_12", "name": "Sales course" } }
```

Collection:

```json
{
  "data": [ { "object": "product", "id": "prd_12" } ],
  "page": { "limit": 50, "next_cursor": "eyJ2IjoxLCJ...", "prev_cursor": null }
}
```

Every resource includes `object` and `id`. A related resource appears as a reference (`{ "object": "product", "id": "prd_12" }`) or as the related public ID. To get the full related resource, retrieve it with its own operation.

## Cursor pagination

```
GET /api/v3/users?limit=100&cursor=eyJ2IjoxLCJ...
```

- `limit` ranges from 1 to 200 and defaults to 50.
- To read the next page, send the `next_cursor` value in the `cursor` parameter. A `null` `next_cursor` means you have reached the end of the collection. There is no total page count and no `offset`.
- The cursor is opaque and signed, and it is valid for 24 hours. A cursor that is malformed, altered, or expired returns `400 invalid_cursor`.
- Reuse a cursor only with the same filters and `sort` value that produced it. Changing them while you page through a collection does not produce consistent results.

Not every collection paginates. Some operations return all matching items in a single response, using the same collection envelope with `next_cursor` set to `null`. If you always follow `next_cursor` until it is `null`, your code handles both cases.

## Filtering and sorting

Each operation in the reference declares the parameters it accepts. Pagination, sorting, and filters are not universal: check the operation before you rely on `limit`, `cursor`, `sort`, or a specific filter.

- Filters are plain query parameters documented per operation, for example `status=published`, `external_id=erp-1001`, `created_after=2026-09-01T00:00:00Z`, or `q=` for text search.
- `sort=` accepts only the values the operation declares. The `-` prefix reverses the order (`sort=-created_at`). An unsupported `limit` or `sort` value returns `422 validation_failed`.
- Dates and times use RFC 3339. Responses always return UTC with `Z` and milliseconds. Requests accept any offset.
- A parameter the operation does not declare returns `400 unknown_parameter`. The API never silently ignores an unknown parameter.
- Collections that support the trash accept `deleted=true` to list items in the trash. To restore an item, send a `PATCH` with `deleted: false`.
- Collections return only what the current credentials can see. Access rules are applied before pagination, so every page contains only accessible items.

For the full list of error codes, see [Errors](https://cademi.dev/api/errors.md).

## Conditional reads and writes

A `GET` for a single resource returns an `ETag`:

```
ETag: W/"product:prd_12:2026-09-21T15:53:20.123456Z"
```

Treat the value as opaque. Store it and send it back in `If-Match` when you write:

```
PATCH /api/v3/products/prd_12
If-Match: W/"product:prd_12:2026-09-21T15:53:20.123456Z"
```

If the resource changed after you read it, the response is `412 revision_mismatch` and nothing is saved. `If-Match` is optional: without it, the write applies to the current version of the resource, and you lose this protection. Send it on every `PATCH` and `PUT .../order` that accepts it, especially on content resources.

## Fields

- `PATCH` is partial: an omitted field is left unchanged, and `null` clears the field when it accepts null.
- `PUT` on a set sub-resource (`/order`, `/tags`, `/content`) replaces the entire set.
- Responses include every field in the schema, with explicit `null` values. A request body field that is not in the schema returns `422 unknown_field`.
- Monetary amounts are strings, returned with a `currency` field alongside. Booleans are never `0` or `1`. Enum values are stable English strings.
