Skip to content
Talk to our solutions team

Query parameters

Three routes build a query from the URL. Everything on this page applies to them and to their anonymous mirrors:

RouteBuilds a query from the URL
GET /data/rest/{entity}yes
GET /data/rest/{entity}/id/{id}yes — the path id becomes the primary-key predicate
POST /data/rest/{entity}/searchyes — the body filter is AND-ed on top
GET /data/anon/rest/{entity}yes
GET /data/anon/rest/{entity}/id/{id}yes
POST /data/anon/rest/{entity}/searchyes

Mutation routes (POST/PUT/PATCH/DELETE /data/rest/{entity}…) and the bulk envelope (/data/rest) parse the behaviour flags only. Query-AST keys and unreserved keys are read and discarded there — they never become filters.

Nothing on this page reaches the rest of the surface, and each of those routes reads the query string its own way: POST /data/graphql reads ?datastore= and nothing else, /data/actions/{name} binds every query parameter as an action parameter on GET and DELETE, /data/x/* hands the whole query string to the script, and each /data/admin/* route reads ?datastore= plus its own parameters. All of those are on Operations.

Route shapes, bodies and status codes are on REST operations. The semantics behind each flag are on Request flags; the AST these parameters build is the query DSL.

RuleDetail
Query-AST keysCase-sensitive, exact match. ?select=, not ?Select=
Behaviour flagsCase-insensitive. ?sendStats=, ?sendstats=, ?SENDSTATS= are one flag
Repeated keyFirst value wins; the rest are dropped
Empty value?status= is skipped entirely, not matched against an empty string
Unknown keyNever a 400. On a read it becomes an equality filter
Non-numeric integer?limit=abc parses to 0 and is ignored

There is no operator syntax in the query string. Every filter is a string-typed equality, and all of them are AND-ed.

FormCompiles to
?status=paid (any unreserved key)"status" = 'paid'
?status=paid&owner_id=42"status" = 'paid' AND "owner_id" = '42'
?filter=status=paid"status" = 'paid' — split on the first =, both sides trimmed
POST …/search body {"filter":{"status":"paid"}}one equality per top-level key, AND-ed onto the URL filters

Values from the query string are always bound as strings. ?total=100 binds '100'; on a numeric column PostgreSQL coerces it, on a column where the cast fails the request returns 500. The search body is the only place a filter value keeps its JSON type, and only for strings, numbers and booleans. A nested object such as {"total":{"$gt":100}} is stringified rather than interpreted as an operator, an array is stringified, and null is bound as the literal text <nil> — never as SQL NULL.

A ?filter= value with no = in it is dropped silently.

A filter on an undeclared column fails the compile before any SQL runs, and the failure is indistinguishable from a database fault on the wire — see Compile failures. That is also how a typo’d flag surfaces; see Parses but never runs.

ParameterValueNotes
selectcomma-separated field namesWins when both are present
fieldscomma-separated field namesAccepted spelling of the same thing

Names are trimmed; empty elements are dropped. Only plain field names are accepted — ?select= has no aggregate, alias or DISTINCT syntax, so ?select=count() is refused as an identifier before SQL is emitted and returns 500.

With no projection the response carries the entity’s declared fields in canonical order — the compiler emits the column list, never SELECT *. With a projection and ?include=, the entity’s primary key is prepended when your list omits it, so the rows come back one column wider than you asked for.

ParameterValue
order_bycomma-separated sort terms — wins when both are present
orderbysame

Each term is field, -field, field desc, or field asc. The direction suffix is matched case-insensitively; the default is ascending.

GET /data/rest/orders?orderby=-created_at,id

Omit the parameter and the compiler still emits an ORDER BY — the entity’s declared default order, otherwise primary key ascending — so pages are stable without one. That injected sort is not visible to the cursor logic; see below.

ParameterApplied whenResult
limit> 0LIMIT <n>
pagesizelimit absent or 0LIMIT <n>
offset> 0OFFSET <n>
pageoffset absent or 0, and a limit resolvedOFFSET (page - 1) * limit

Page numbers are 1-based, so ?page=1 produces no offset. ?page=3 with no limit and no pagesize is ignored silently — there is nothing to multiply by.

GET /data/rest/orders?page=3&pagesize=25

The engine injects a 10,000-row cap: a request with no limit gets LIMIT 10000, and a limit above the cap is lowered to it. A capped result looks exactly like a complete one — there is no marker in the response.

ParameterValue
afterforward cursor
beforebackward cursor

The meaning depends on whether you sent ?orderby=.

With ?orderby=Without ?orderby=
The value is decoded as an opaque keyset token and compiled to a keyset predicate over the sort columnsThe value is bound verbatim as a primary-key bound: afterpk > '<value>', beforepk < '<value>'

Tokens come back in the envelope’s page block:

{"data":[],"count":25,"page":{"has_next_page":true,"end_cursor":"eyJ2IjoxLCJmIjpb…"}}

page is emitted only when a cursor could be minted, which requires rows in the result and an explicit ?orderby=. The injected default sort does not qualify, so a read without ?orderby= never returns end_cursor. has_next_page is a heuristic — it is true when the page came back full, so the last page of an exact multiple reports one more page than exists.

ParameterValue
includecomma-separated relation names declared on the entity
GET /data/rest/orders?include=customer,items&limit=20

Each relation is fetched by a separate statement and grafted onto the parent rows under the relation name. Over HTTP only the relation name is expressible: there is no query-string syntax for a child projection, child filter, child sort, child limit, or nesting. ?include=user.org is read as one relation named user.org and fails.

An unrecognised relation fails the compile like an unknown field does — 500, code v2.query_failed, generic message. See Compile failures.

ParameterValue
groupbycomma-separated column names

GROUP BY is emitted, and the default sort injection is skipped for grouped reads. Because ?select= cannot express an aggregate, a grouped read that projects any column outside the group list fails in the database, not in the compiler. Grouped reads are practical from a declared action or a scripted endpoint, not from the query string.

Case-insensitive, truthy set true, t, yes, y, on, 1. Full semantics on Request flags.

FlagValueEffect on a read
deletedboolDrops the injected soft-delete predicate so deleted rows are returned
datastorenameSelects the tenant datastore; omit for the schema default
versionN, first, latest, latest-N, -NPins the version on entities with a versioned history strategy
expressionjq programReplaces the response body with the result of the program run over the whole envelope
unmaskboolStacks the entity’s unmask access rule; does not change the body
untokenizeboolStacks the entity’s decrypt access rule; does not change the body
downloadtrueGet-by-id only: switches to file streaming. Returns 501 v2.file_downloader_unconfigured; without an id, 400 v2.download_needs_id
fieldfield nameNames the file field for download

On a get-by-id, a soft-deleted row without ?deleted=true returns 404 v2.row_not_found.

?deleted=true carries no permission of its own — any caller with read on the entity can use it, including over /data/anon/rest/{entity}.

The mutation-side flags — purge, delta, deletechildren, noversion, scd/cdc, customstateflow, event, sendhash, alias — are parsed on read routes too and change nothing there. skipstateflow is the exception: it is not reserved, so on a read it becomes a filter and fails the request. See Parses but never runs.

FlagSpellingsAdds
statsstats, sendstatsstats: sql, duration_ms, include_sql[], rows_affected
metameta, sendmetameta: entity, primary_key, fields[] (name, type, required, nullable)
statesstates, sendstatesstates[]: one entry per row, keyed by stateflow name, each {field, current, events[]}. Omitted entirely when the entity declares no stateflow
allowedactionsallowedactions, sendallowedactionsallowed_actions[]: which of create, read, update, delete, purge, restore, unmask, decrypt the entity declares an access rule for — the fixed four create, read, update, delete when it declares none

allowed_actions is read off the entity declaration, not evaluated against the caller — a name in the list is not a promise the request will pass its access rule.

count is always present and is the number of rows in data. total and stats.total_count are not populated on reads — the engine never computes a total count, so there is no way to get a match count alongside a page. On a mutation that returns no rows, total carries the affected-row count instead.

ParameterWhat happens
havingReserved, so it is not turned into a filter — and then never read. No HAVING is emitted, no error is returned
depthReserved and never read. Include nesting is not controllable from the query string
asofParsed into a point-in-time option and left out of the reserved set, so it also becomes a filter on a column named asof. See below
skipstateflowSame defect: parsed as a flag and also filtered on
downloadRoutes to the file path, which returns 501 in the shipping service
free-text query in the search bodyReturns 501 v2.search_text_not_supported
string filter in the search bodyReturns 501 v2.search_dsl_not_supported
ConditionStatusCode
Undeclared field in select, orderby, groupby or a filter500v2.query_failed / v2.search_query_failed
Unknown relation in include500v2.query_failed / v2.search_query_failed
A value that is not a plain identifier, such as select=count()500v2.query_failed / v2.search_query_failed
Entity not in the addressed schema404v2.entity_not_found
Get-by-id matched nothing404v2.row_not_found
Access denied on the read403v2.query_failed / v2.search_query_failed
expression fails to compile400v2.bad_expression
expression fails at runtime500v2.expression_runtime
Search body is not JSON400v2.search_body_parse_failed
Search body carries query501v2.search_text_not_supported
Search body filter is a string501v2.search_dsl_not_supported

Access denial is the exception that keeps its status: the request-time access gate returns a typed error that reaches the HTTP layer intact, so it maps to 403. Status selection for the errors that survive sanitising is partly substring matching on the message text, so branch on code, never on the status.

Filtered, projected, sorted, paged list:

Terminal window
curl -G https://<host>/data/rest/orders \
-H "Authorization: Bearer $TOKEN" \
-H "X-Customer: acme" -H "X-Product: erp" -H "X-Env: prod" -H "X-Tenant: main" \
--data-urlencode "status=paid" \
--data-urlencode "select=id,total,status,created_at" \
--data-urlencode "orderby=-created_at,id" \
--data-urlencode "limit=25"

Follow the cursor, sending the same sort back:

Terminal window
curl -G https://<host>/data/rest/orders \
-H "Authorization: Bearer $TOKEN" \
-H "X-Customer: acme" -H "X-Product: erp" -H "X-Env: prod" -H "X-Tenant: main" \
--data-urlencode "orderby=-created_at,id" \
--data-urlencode "limit=25" \
--data-urlencode "after=eyJ2IjoxLCJmIjpb..."

Structured search, with the URL supplying pagination and projection:

Terminal window
curl -X POST "https://<host>/data/rest/orders/search?limit=50&select=id,total" \
-H "Authorization: Bearer $TOKEN" \
-H "X-Customer: acme" -H "X-Product: erp" -H "X-Env: prod" -H "X-Tenant: main" \
-H "Content-Type: application/json" \
-d '{"filter":{"status":"paid","region":"eu"}}'

Soft-deleted rows from a second datastore, reshaped with jq:

Terminal window
curl -G https://<host>/data/rest/invoice \
-H "Authorization: Bearer $TOKEN" \
-H "X-Customer: acme" -H "X-Product: erp" -H "X-Env: prod" -H "X-Tenant: main" \
--data-urlencode "deleted=true" \
--data-urlencode "datastore=reporting" \
--data-urlencode "expression=.data | map({id, deletedon})"