Skip to content
Talk to our solutions team

Request flags

data.svc parses 23 flags off the query string on its REST routes: 18 that travel with the request through the engine and hook chain, 4 that only change the shape of the response envelope, and download, which switches a get-by-id into file streaming. On a read, every query-string key that is not one of these (and not a query-AST key such as select or limit) becomes an equality filter predicate instead.

Route (public path)Engine/routing flagsResponse-shape flags
GET /data/rest/{entity}allall
GET /data/rest/{entity}/id/{id}allall
POST /data/rest/{entity} (JSON or multipart)allall
PUT/PATCH /data/rest/{entity}[/id/{id}]allall
DELETE /data/rest/{entity}/id/{id}allall
POST /data/rest/{entity}/id/{id}/restoreallall
POST /data/rest/{entity}/searchallall
POST/PUT/PATCH/DELETE /data/rest (bulk envelope)parsed; datastore and purge take effect, delta and expression do notstats only
GET/POST/… /data/anon/rest/*allall

The anonymous mirror parses exactly the same flags as the authenticated routes; it has no restore path. GraphQL, /data/actions/{name}, the scripted /data/x/* endpoints and every admin route read ?datastore= only — none of the other flags reach them. Route shapes, headers and status codes are in the Data API HTTP reference.

  • Keys are case-insensitive. ?sendStats=true, ?sendstats=true and ?SendStats=true reach the same field. The one exception is the file-download path, which reads download and field with an exact, lower-case key lookup.
  • Truthy values are true, t, yes, y, on, 1 (any case). Everything else — including ?deleted with no value — is false.
  • Unknown keys are never rejected. A request carrying a flag the service does not know gets no 400; on a read it becomes a filter predicate.
  • A repeated key uses its first value.
  • A malformed asof is dropped silently and the read falls back to current-row semantics.
FlagSpellingsValueDefaultEffect
deleteddeletedboolfalseDrops the auto-injected deletedon IS NULL predicate so soft-deleted rows come back. Reads only; no effect on entities without soft delete.
unmaskunmaskboolfalseAsks for unmasked values on a read and stacks the entity’s unmask access rule. Does not change the body in the shipping service — see below.
untokenizeuntokenizeboolfalseAsks for detokenised values on a read and stacks the entity’s decrypt access rule (not an untokenize rule). Does not change the body in the shipping service.
purgepurgeboolfalseTurns a delete into a hard DELETE instead of a soft-delete UPDATE, and bypasses version bookkeeping on versioned entities. Stacks the entity’s purge access rule.
deltadeltaboolfalseOn create and update, trims every returned row to the literal key id plus the keys present in the request body. Reads, deletes and restores pass through untrimmed.
datastoredatastoredatastore nameschema defaultSelects which of the tenant’s datastores serves the request.
expressionexpressionjq programnoneRuns jq over the composed envelope and replaces the response body with the result.
versionversionN, first, latest, latest-N, -Ncurrent rowPins the version read on entities with a versioned history strategy.
asofasoftimestampcurrent rowPoint-in-time read on versioned entities. Unusable on REST reads — see the caution below.
fieldfieldfield namenoneNames the file field on ?download=true. Inert on every other path, and inert there too — see download.
downloaddownloadtruefalseRouting switch, not an engine flag: on a get-by-id path it streams a file field instead of returning the row. Matches only the literal value true (any case), not the truthy set above. Returns 501 v2.file_downloader_unconfigured in the shipping service — no downloader is registered at boot. On a list path without an id it returns 400 v2.download_needs_id.
scdscd, cdcboolunsetRecords an explicit enable/disable of history bookkeeping for the mutation. No hook consumes it; nothing changes.
sendhashsendhashboolfalseNo effect — no hook consumes it.
deletechildrendeletechildrenboolfalseNo effect — no cascade-delete hook is registered.
noversionnoversionboolfalseNo effect — no hook consumes it.
skipstateflowskipstateflowboolfalseNo effect on mutations; breaks reads — see the caution below.
customstateflowcustomstateflowfield:flow,field2:flow2noneParsed into a map. No hook consumes it; stateflows are not swapped.
eventeventevent namenoneNo effect. The stateflow machine derives the event from the target state in the payload.
aliasaliasentity aliasnoneNo effect.

Seven of these change behaviour today: deleted, purge, delta, datastore, expression, version, and the access-guard half of unmask/untokenize. asof never reaches the engine over REST and field never reaches the file path — see the cautions below. The rest parse cleanly, travel on the request context, and are mirrored into the hook chain’s metadata map, but no hook in the deployable service reads them. They are a stable contract for a host that installs its own hooks — not behaviour you get by sending them.

?version= accepted forms:

FormMeaning
N (positive integer)exact _version_num = N
firstversion 1
latestthe current row — same as omitting the flag
latest-N, -NN versions back from the row’s current _version_num

latest-N needs a primary-key predicate in the request; without one, or when the token is malformed, the read falls back to the current view. Non-versioned entities ignore the flag.

FlagSpellingsAdds to the envelope
statsstats, sendstatsstats: sql, duration_ms, include_sql[], rows_affected
metameta, sendmetameta: entity, primary_key, fields[] with name, type, required, nullable
statesstates, sendstatesstates: one object per row, keyed by stateflow name, each {field, current, events[]}
allowedactionsallowedactions, sendallowedactionsallowed_actions[]: the action names declared on the entity

states is omitted entirely when the entity declares no stateflows, so a client cannot tell “no stateflows” from “flag not sent” without checking the schema.

allowed_actions lists the actions that have a rule declared on the entity — create, read, update, delete, purge, restore, unmask, decrypt — not the actions this caller is permitted to perform. An entity with no access.actions block returns the fallback list ["create","read","update","delete"]. Render buttons from it if you like, but the 403 still arrives from the access gate.

On the bulk envelope only stats is honoured, and it carries a different shape: {operations_run, operations_ok, operations_fail}.

?unmask=true and ?untokenize=true do two separate things, and only the first of them is live in the deployable service.

The access guard runs. On a read, unmask stacks the entity’s unmask rule and untokenize stacks its decrypt rule on top of the base read rule, with AND logic — a caller who passes read but fails unmask is denied. The extended guard is evaluated only when the entity declares that rule. An entity with a read rule and no unmask rule admits ?unmask=true on the strength of the read rule alone. Declare the rule if you want the flag gated; see Access rules.

The reveal does not. Two components implement mask/redact/hidden — a compliance hook that runs after the scan, and a compiler-side output filter. data.svc registers neither on its read path: the hook is absent from its chain, and the output filter is constructed at boot but never invoked. Nothing on the read path applies or lifts those transforms, so neither flag changes a single byte of the response body. A host that builds its own hook chain can install the compliance hook and get the reveal behaviour; data.svc as shipped does not.

Treat the guards as the enforceable contract and do not treat the absence of ?unmask=true as a redaction control.

?deleted=true has no permission of its own. Any caller allowed to read the entity can retrieve its soft-deleted rows by adding the flag, including over the anonymous mirror at /data/anon/rest/{entity}. If deleted rows are sensitive, constrain them with a row-level security predicate rather than assuming the default filter hides them.

?purge=true marks the delete as a hard delete, which makes the compiler emit a real DELETE rather than the soft-delete UPDATE, and routes versioned entities around version bookkeeping — no closing row is written, so the history gap is not recorded. The purge access rule is stacked only when the entity declares one; otherwise delete permission is enough.

The bulk envelope carries its own per-row purge key inside data. The query-string flag is read from the request context by the engine, so ?purge=true on the bulk URL applies to every delete operation in the batch.

?stats=true returns the compiled SQL text in stats.sql, plus one entry per eager-load query in stats.include_sql. Bind values are not included — the SQL carries placeholders — but the table names, column list, injected tenancy and row-level-security predicates are all visible to anyone who can read the entity. Keep the flag out of browser-facing clients.

stats.total_count and the envelope’s total are not populated on reads: the engine never sets a total count. On a mutation with no returned rows, total carries the affected-row count instead.

?expression= runs a jq program in-process over the fully composed envelope, so .data, .count and .stats are addressable and the result replaces the body wholesale.

BehaviourValue
Evaluation ceiling2 seconds, hard
Compiled-program cache256 entries, FIFO eviction
Zero resultsnull
More than one resultJSON array of the results
Compile error400, code v2.bad_expression
Runtime error500, code v2.expression_runtime

It runs on every REST route that emits the standard envelope, including create and update responses. It does not run on the bulk envelope.

Read soft-deleted rows of one entity, project them down to two fields with jq, and ask for the compiled SQL:

Terminal window
curl -G https://api.example.com/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 "limit=50" \
--data-urlencode "stats=true" \
--data-urlencode "expression={sql: .stats.sql, rows: (.data | map({id, total}))}"

The jq program runs over the whole envelope, so it has to re-project anything it wants to keep — .data | map(...) on its own would discard the stats block you just asked for.

Create a row and get back only what you sent:

Terminal window
curl -X POST "https://api.example.com/data/rest/invoice?delta=true" \
-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 '{"customer_id":"01H...","total":420}'
{"data":[{"id":"01J...","customer_id":"01H...","total":420}],"count":1}