POST /api/{entity}/search) takes the same four inputs: filter, sort, page_size and cursor, plus an optional include list.
Paging
page_sizedefaults 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 avalidation_failedrather than a clamp.- Responses return
pagination.next_cursor, an opaque string. Pass it back ascursorto get the next page;nullmeans the last page. Cursors are forward-only, and there is no offset or page number. page_size: 0is count-only mode:itemscomes back empty and you readcounts.total_count. It is the cheapest way to answer “how many match”.page_size: 1is 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.
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 overPOST 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.