Skip to main content
Every list endpoint (POST /api/{entity}/search) takes the same four inputs: filter, sort, page_size and cursor, plus an optional include list.

Paging

  • page_size defaults to 50. The ceiling is per endpoint, 100, 200, 500 or 1000 depending on how heavy the table is; each endpoint’s reference states its own range, and asking for more than it allows is a validation_failed rather than a clamp.
  • Responses return pagination.next_cursor, an opaque string. Pass it back as cursor to get the next page; null means the last page. Cursors are forward-only, and there is no offset or page number.
  • page_size: 0 is count-only mode: items comes back empty and you read counts.total_count. It is the cheapest way to answer “how many match”.
  • page_size: 1 is the get-first idiom: one row plus a cursor, for “does anything match, and what is the newest one”.

Filtering

filter is a typed object, validated per entity. Each filterable field takes an object of operators; which operators a field supports is spelled out in the reference for that endpoint.

The operator vocabulary

Nine operators exist. Which ones a given field accepts is stated in the reference for that endpoint, and asking for one a field does not accept is an error rather than a silent no-op. Two rules that are easy to miss:
  • Multiple operators on one field are ANDed. {"created_at": {"gte": "...", "lt": "..."}} is a half-open range, which is how you page a window by time rather than by cursor.
  • A bare scalar is shorthand for eq. {"status": "active"} and {"status": {"eq": "active"}} mean the same thing.
q is full-text search where the entity supports it. It is a reserved field inside filter, not a top-level parameter, and each endpoint’s description names the columns it scans.
An unknown filter field, an unsupported operator, or a value of the wrong type answers validation_failed with a field_errors entry naming the offending path. Nothing is silently dropped, so a filter that returns everything is a filter you did not send, not one that was ignored.
The response echoes what actually ran in applied_filters, which is the fastest way to confirm a filter reached the backend in the shape you meant.

Sorting

sort is a single object with a field and an optional direction, for example { "field": "created_at", "direction": "desc" }. Direction defaults to desc, and one sort field applies per request. The sortable fields are listed per endpoint.

Includes

include names relations to load with each row, for example ["metrics"] on API keys. Loaded relations arrive under items[].included, keyed by relation name, and the response’s top-level includes echoes what you asked for. If a requested relation is absent under included for a row, that row has none; if it was never in includes, it was not requested. The two cases stay distinguishable.

Objects in query strings

Search runs over POST with a JSON body, so this mostly concerns GET and DELETE endpoints that accept the same parameters:
  • An object parameter (filter, sort) travels as JSON text in the query string: ?filter={"status":{"eq":"active"}} (URL-encoded).
  • An array parameter repeats as name[]=value: ?include[]=metrics&include[]=owner.
The playground on each reference page builds these for you, which is the quickest way to see the exact wire format.