Pagination, filtering, sorting, includes

The query parameters every list endpoint understands — page[number]/page[size], the filter grammar and operators, sort, include, sparse fieldsets — plus the changes feed and optimistic locking.

Pagination#

GET /api/customers?page[number]=2&page[size]=50
ParameterDefaultMaximum
page[number]1— (≤ 0 clamps to 1)
page[size]20200 (larger values clamp silently; meta.pageSizeClamped is set)

Only the bracket form is honoured; pageNumber/pageSize fall back to defaults. Responses carry meta (totalCount, currentPage, pageSize, totalPages) and links (self, prev, next — relative URLs that preserve your other parameters). metadata duplicates meta for older clients and is deprecated.

Filtering#

filter[<field>][<op>]=<value>
filter[<field>]=<value>          # shorthand for [eq]
OperatorMeaning
eq, neqequals / not equals
gt, gte, lt, ltecomparisons (numbers, dates)
inany of a comma-separated list — filter[status][in]=SENT,PAID (alias any)
contains, starts, endssubstring, prefix, suffix
icontains, istarts, iends, ieqcase-insensitive variants
isnulltrue / false

Multiple filters combine with AND. There is no OR across fields — run two queries and merge, or use a saved list view with a formula. Any other filter… key shape (filter[x][in][]=…, indexed variants) is rejected with 400 INVALID_QUERY rather than silently ignored.

Filtering on a field that is masked for the caller answers 403 MASKED_FIELD_PREDICATE; formula fields cannot be filtered.

Sorting#

GET /api/invoices?sort=-due_date,number

Comma-separated fields, - prefix for descending. Sorting on a masked field is rejected like filtering.

Includes#

GET /api/invoices?include=customer            # to-one: name the lookup field
GET /api/customers?include=invoices           # has-many: name the child collection
GET /api/customers?include=invoices,invoice-lines   # grandchildren resolve through the included child

Included resources land in included[] and obey field-level security. See Relationships.

Sparse fieldsets#

GET /api/invoices?fields[invoices]=number,amount&include=customer&fields[customers]=name

Changes feed (sync)#

GET /api/invoices/_changes?since=2026-09-01T00:00:00Z

Returns record deletions since the cursor (from tombstones) plus a fresh cursor. Fetch upserts with the normal list endpoint: filter[updatedAt][gt]=<cursor>&sort=updatedAt. This is what the app’s offline mode uses.

Optimistic locking#

Single-record reads return an ETag. Send it back as If-Match on PATCH or DELETE to have the write rejected with 409 if the record changed in between. The kanban board uses this when it moves cards.

GET /api/_search?q=… across collections and POST /api/<collection>/semantic-search. Both return only records the caller can read — see Search and attachments.

Aggregates and the latest value#

Two read-only siblings of the list endpoint answer a question without returning rows. Both take the same filter[…] parameters and need only read on the collection.

  • GET /api/<collection>/aggregate?groupBy=<field>&avg=<numericField> — one group per distinct value of groupBy, with its count and (when avg is given) the average: { "totalCount": 42, "groups": [{ "key": "open", "count": 30, "avg": 4.5 }, …] }. Grouping is exact-value and in memory over at most 1,000 matching rows.
  • GET /api/<collection>/latest?fields=<f1,f2> — the named fields of the most recently created matching record, plus its recordedAt: { "record": { "status": "ok", "recordedAt": "…" } }, or { "record": null }.

Neither response is a JSON:API data list: they disclose only counts, averages or the fields asked for.

Edit this page on GitHub