REST
The entity REST surface is generated at request time from the addressed tenant’s schema. The routes below exist for every entity that schema declares; nothing is pre-generated per entity.
Concepts live in the block documentation and are not repeated here — the filter and sort grammar in Query DSL, the flag semantics in Request flags, and the full code table in Error codes.
Routes
Section titled “Routes”Paths are the public ones. The gateway matches the prefix /data/ and
strips it before forwarding, so the service itself serves /rest/{entity}; behind the gateway you
address /data/rest/{entity}.
| Method | Path | Purpose | Auth |
|---|---|---|---|
GET | /data/rest/{entity} | List rows | Bearer or API key |
GET | /data/rest/{entity}/id/{id} | Read one row, or stream a file field | Bearer |
POST | /data/rest/{entity} | Create one row (JSON or multipart) | Bearer |
PUT | /data/rest/{entity} | Update, primary key in the body | Bearer |
PUT | /data/rest/{entity}/id/{id} | Update, primary key in the path | Bearer |
PATCH | /data/rest/{entity} | Partial update, primary key in the body | Bearer |
PATCH | /data/rest/{entity}/id/{id} | Partial update, primary key in the path | Bearer |
DELETE | /data/rest/{entity}/id/{id} | Delete (soft by default) | Bearer |
POST | /data/rest/{entity}/id/{id}/restore | Undo a soft delete | Bearer |
POST | /data/rest/{entity}/search | Structured filter search | Bearer |
POST | /data/rest | Bulk operations envelope, default action create | Bearer |
PUT | /data/rest | Bulk operations envelope, default action update | Bearer |
PATCH | /data/rest | Bulk operations envelope, default action update | Bearer or API key |
DELETE | /data/rest | Bulk operations envelope, default action delete | Bearer |
POST | /data/bulk | Import rows into one entity from CSV/JSON/JSONL | Bearer |
PATCH | /data/bulk | Export rows from one entity | Bearer or API key |
Every /data/rest route above is mirrored under /data/anon/rest/… with no token, except restore;
/data/bulk has no anonymous mirror. See Anonymous mirror.
Request headers
Section titled “Request headers”| Header | Required | Value |
|---|---|---|
X-Customer | yes | Customer slug |
X-Product | yes | Product slug |
X-Env | yes | Environment slug |
X-Tenant | yes | Tenant slug — the tenant segment only, not the four-part key |
Authorization | yes, except the anonymous mirror | Bearer <token> from iam.svc |
Content-Type | on bodies | application/json, or multipart/form-data on create |
If-Match | optional, on update and delete | The row’s concurrency token as returned in ETag, quoted. Only entities with a concurrency strategy can check it; * is ignored |
The four tenant headers are assembled into one tenant key by middleware that runs before the
handler. A request missing or malformed on any of them is rejected with 400 invalid tenant
before routing; a request that reaches a handler with no resolved tenant returns 403
no_tenant.
An expired or invalid token is rejected by the authentication middleware, which writes plain text
Unauthorized with Content-Type: text/plain; charset=utf-8 — not the JSON error envelope.
Three routes accept a static API key in place of a bearer token: GET /data/rest/{entity},
PATCH /data/rest and PATCH /data/bulk. Every other route requires a bearer token.
Response headers
Section titled “Response headers”| Header | When | Value |
|---|---|---|
Content-Type | always | application/json; charset=utf-8, or the file’s own type on a download |
X-RateLimit-Limit | a limiter is installed | Requests allowed in the window |
X-RateLimit-Remaining | a limiter is installed | Requests left |
X-RateLimit-Reset | the limiter reports a reset time | Window reset, unix seconds |
Retry-After | 429 | Seconds until retry, rounded up, minimum 1 |
Content-Disposition | file download, CSV export | attachment; filename="…" |
ETag | single-row responses on an entity with a concurrency strategy | The row’s token, quoted — hand it back as If-Match |
Rate limiting applies when a limiter is configured; without one, requests pass and no
X-RateLimit-* headers are set. Configure a limiter, or place one at your edge, for any surface
reachable by callers you do not control. A denial is 429 with code rate_limited and details carrying profile,
limit, remaining and retry_after.
Response envelope
Section titled “Response envelope”Every REST route except the file download and the export route returns the same object. The bulk
envelope returns one such object per operation (see Bulk operations envelope).
Rows live under data; everything about the rows lives under meta; failures under errors;
non-fatal notices under warnings.
| Key | Type | When |
|---|---|---|
data | array of objects | Always. A single-row read or write still returns a one-element array |
meta | object | Always. Everything about the rows — see the table below |
errors | array of {code, message, details?, path?} | Only when something failed. A REST route that fails has an empty data and a non-2xx status |
warnings | array of {code, message, path?} | Only when the request succeeded with something worth telling you |
Keys under meta:
| Key | Type | When |
|---|---|---|
entity | string | Always |
count | int | Always. Rows in data for this response, not the table total |
total | int | Reads only, when the engine reports a total count |
affected | int | Mutations only — rows the statement touched, including 0 |
page | object | Cursor pagination is active — {has_next_page, end_cursor} |
stats | object | ?stats=true — sql, duration_ms, total_count, include_sql[], rows_affected |
schema | object | ?meta=true — entity, primary_key, fields[] of {name, type, required, nullable} |
states | array | ?states=true — one object per row, keyed by stateflow name, each {field, current, events[]} |
allowed_actions | array of strings | ?allowedactions=true — actions the entity declares an access rule for; create,read,update,delete when it declares none |
{ "data": [ { "id": "01JC8…", "code": "EUR", "name": "Euro", "symbol": "€", "fraction_units": 100 } ], "meta": { "entity": "currency", "count": 1 }}?expression=<jq> replaces the body wholesale: the program runs over the composed envelope above,
so .data, .meta.count and .meta.stats are addressable. Zero results yield null, multiple results
yield an array. A program that fails to compile returns 400 bad_expression; one that fails at
run time, or exceeds the 2-second evaluation ceiling, returns 500 expression_runtime.
Error envelope
Section titled “Error envelope”{ "errors": [ { "code": "entity_not_found", "message": "entity ordr not found in schema" } ] }errors is always an array; code and details are omitted when empty. Branch on code, not on the status — six codes
(query_failed, create_failed, update_failed, delete_failed,
restore_failed, search_query_failed) name the operation that failed and carry a status
chosen from the underlying engine error: 403 on an access denial, 404 on a missing row or entity,
422 on a validation failure, 500 otherwise.
An unrouted path returns a bare 404 with no body — the router’s not-found handler writes no JSON.
GET /data/rest/{entity}
| Path parameter | Required | Description |
|---|---|---|
entity | yes | Entity name as declared in the schema |
Query construction
Section titled “Query construction”| Parameter | Spellings | Effect |
|---|---|---|
select | select, fields | Projection, comma-separated. select wins |
orderby | orderby, order_by | Sort, comma-separated. -field, or a trailing desc / asc |
limit | limit, pagesize | Row limit. limit wins |
offset | offset | Row offset |
page | page | 1-based. Requires a limit; converted to offset = (page-1) × limit. Ignored with an explicit offset |
after / before | Opaque cursor, passed through verbatim | |
include | Relations to expand, comma-separated | |
groupby | GROUP BY columns, comma-separated | |
filter | Legacy ?filter=<key>=<value>, split on the first = and AND-ed as an equality | |
| any unreserved key | field = '<value>', all AND-ed together |
Everything else — the flags that change engine behaviour (deleted, purge, delta,
datastore, version, unmask, untokenize, …) and the response-shape flags (stats, meta,
states, allowedactions) — is enumerated with its real effect in
Request flags.
Filtering
Section titled “Filtering”Any query-string key that is not reserved becomes an equality predicate, AND-ed with the rest.
?status=paid®ion=eu compiles to WHERE status = 'paid' AND region = 'eu'.
GET /data/rest/order?status=paid&select=id,total,status&orderby=-created_at&limit=10&include=customerThe reserved keys, which are consumed rather than turned into predicates:
select orderby order_by limit offset page pagesizeafter before include filter fields groupby havingdepth deleted stats sendstats meta sendmeta statessendstates allowedactions sendallowedactions sendhash unmaskuntokenize purge deletechildren noversion delta scdcdc customstateflow event field expression aliasversion datastore downloadMatching is case-insensitive, so ?sendStats=, ?sendstats= and ?SENDSTATS= are the same key.
Status codes
Section titled “Status codes”| Status | Code | Condition |
|---|---|---|
| 200 | — | Success, including zero matching rows (count: 0) |
| 400 | bad_path | Entity path segment empty |
| 403 | no_tenant | No tenant resolved from the request |
| 404 | entity_not_found | Entity not declared in the resolved schema |
| 403 / 404 / 422 / 500 | query_failed | Engine error; status from the underlying failure |
| 500 | engine_unavailable | Tenant engine, datastore or pool could not be resolved |
Read one row
Section titled “Read one row”GET /data/rest/{entity}/id/{id}
Takes the same query parameters as the list route. The row is matched on the entity’s declared primary key.
| Status | Code | Condition |
|---|---|---|
| 200 | — | One-element data array. On an entity with a concurrency strategy the response carries ETag: "<token>" |
| 404 | row_not_found | No matching row — including a soft-deleted one, which is invisible without ?deleted=true |
A 404 immediately after a successful DELETE is expected: delete is soft by default.
File download
Section titled “File download”GET /data/rest/{entity}/id/{id}?download=true&field={fieldname}
?download=true re-routes this path to stream one file-typed field’s bytes instead of returning
the row. The response carries the stored Content-Type, Content-Disposition: attachment; filename="…", and Content-Length when the size is known. download matches only the literal
value true (any case), not the broader truthy set the other flags accept.
| Status | Code | Condition |
|---|---|---|
| 400 | download_needs_id | ?download=true on a list path with no row id |
| 400 | missing_field | ?field= absent |
| 400 | field_not_file | The named field is not file-typed |
| 404 | field_not_found | The entity has no such field |
| 404 | field_empty | The field holds no stored reference |
| 501 | file_downloader_unconfigured | No downloader wired at boot |
Create
Section titled “Create”POST /data/rest/{entity}
The JSON body is the row payload — there is no wrapper key. Returns 201 with the created
row (all hooks applied) as a one-element data array.
curl -sX POST https://<host>/data/rest/currency \ -H 'Authorization: Bearer <token>' \ -H 'Content-Type: application/json' \ -H 'X-Customer: acme' -H 'X-Product: forge' -H 'X-Env: prod' -H 'X-Tenant: main' \ -d '{"code":"EUR","name":"Euro","symbol":"€","fraction_units":100}'{ "data": [ { "id": "01JC8…", "code": "EUR", "name": "Euro", "symbol": "€", "fraction_units": 100 } ], "meta": { "entity": "currency", "count": 1 }}| Status | Code | Condition |
|---|---|---|
| 201 | — | Created. On an entity with a concurrency strategy the response carries ETag: "<token>" |
| 400 | bad_json | Body is not a parseable JSON object |
| 404 | entity_not_found | Entity not in the schema |
| 403 / 404 / 409 / 422 / 500 | create_failed | Engine error, including every validation failure — a reference to a missing parent is 422 reference_missing; a unique violation is 409 |
Multipart create
Section titled “Multipart create”Send Content-Type: multipart/form-data to the same route to upload file fields alongside scalar
values. Text parts map onto payload fields; a value beginning with {, [, ", -, t, f,
n or a digit is parsed as a JSON literal first and falls back to the literal string. File parts
map onto file-typed fields as {name, content_type, size, data}. The in-memory cap is 32 MiB;
beyond that the parse spills to disk.
| Status | Code | Condition |
|---|---|---|
| 400 | multipart_parse | Malformed multipart body |
| 400 | multipart_empty | No parts |
| 400 | multipart_bad_payload | File part for an undeclared field, a non-file-typed field, or more than one file on one field |
| 501 | file_storage_unconfigured | No storage wired at boot |
Update
Section titled “Update”PUT and PATCH run the same handler and have the same semantics: only the fields present in
the body are written. There is no full-replacement variant — a PUT with a partial body is a
partial update.
| Shape | Route | Primary key from |
|---|---|---|
| Path id | PUT|PATCH /data/rest/{entity}/id/{id} | The {id} path segment |
| Body id | PUT|PATCH /data/rest/{entity} | The primary-key field in the body |
The primary key is stripped from the SET clause and used only as the filter, so an entity whose
key is marked immutable still updates cleanly. A body key that contradicts the path id is
refused rather than ignored.
On an entity with a concurrency strategy the
update may carry the token the caller read — If-Match: "<token>", or "expect": {"version": 7}
/ "expect": {"updatedon": "…"} in the body (expect is not written to the row). The update
applies only if the row still carries that token; the response ETag is the new one.
curl -sX PATCH https://<host>/data/rest/currency/id/01JC8… \ -H 'Authorization: Bearer <token>' \ -H 'Content-Type: application/json' \ -H 'X-Customer: acme' -H 'X-Product: forge' -H 'X-Env: prod' -H 'X-Tenant: main' \ -d '{"name":"Euro (updated)"}'| Status | Code | Condition |
|---|---|---|
| 200 | — | Updated; the row is returned as a one-element data array, with ETag on a concurrency entity |
| 400 | bad_json | Body is not a parseable JSON object |
| 400 | missing_id | Body-id shape with no primary key in the body |
| 400 | bad_precondition | If-Match or expect is not a token |
| 400 | precondition_conflict | If-Match and expect disagree |
| 422 | id_mismatch | Body primary key differs from the path id |
| 412 | update_failed | The row carries another token: details.code: version_mismatch, details.expected, details.current |
| 428 | update_failed | The entity requires a token and none was sent: details.code: precondition_required |
| 400 / 403 / 404 / 409 / 422 / 500 | update_failed | Engine error; 400 with details.code: precondition_unsupported / precondition_invalid when the token cannot be checked |
Delete and purge
Section titled “Delete and purge”DELETE /data/rest/{entity}/id/{id}
Delete is soft by default: the row is retained with its delete markers stamped and stops being
returned by reads. ?purge=true makes it a hard DELETE, bypasses version bookkeeping, and
stacks the entity’s purge access rule.
| Query parameter | Effect |
|---|---|
purge | Hard delete instead of soft |
datastore | Target datastore |
stats | Add the stats block |
What happens to rows that reference the deleted one is declared on the
reference (onDelete): restrict refuses the
delete, cascade deletes the children the same way, setnull clears their column. On a
concurrency entity the delete may carry
If-Match: "<token>" (or expect in a JSON body) and applies only if the row still carries it.
| Status | Code | Condition |
|---|---|---|
| 200 | — | Deleted |
| 400 | bad_path | Entity or id segment empty |
| 400 | bad_precondition / precondition_conflict | The token does not parse, or If-Match and expect disagree |
| 409 | delete_failed | Child rows still reference the row: details.code: reference_restrict, details.child_entity, details.count |
| 412 | delete_failed | The row carries another token: details.code: version_mismatch |
| 428 | delete_failed | The entity requires a token and none was sent |
| 400 / 403 / 404 / 422 / 500 | delete_failed | Engine error |
A soft delete compiles to an UPDATE, so the returned row is the post-image with the delete
markers set. A purge returns the row as it last existed.
Restore
Section titled “Restore”POST /data/rest/{entity}/id/{id}/restore
Undoes a soft delete. No body required. The caller must hold the entity’s restore action
permission; the access gate enforces it.
| Status | Code | Condition |
|---|---|---|
| 200 | — | Restored |
| 400 | bad_path | Entity or id segment empty |
| 403 / 404 / 422 / 500 | restore_failed | Engine error, including an entity with no soft-delete trait |
Search
Section titled “Search”POST /data/rest/{entity}/search
A body-driven filter that is AND-ed onto a query otherwise built from the query string — so pagination, sort, projection and includes still come from the URL and apply here unchanged.
{ "filter": { "status": "shipped", "region": "eu" }}curl -sX POST 'https://<host>/data/rest/order/search?limit=20&orderby=-created_at' \ -H 'Authorization: Bearer <token>' \ -H 'Content-Type: application/json' \ -H 'X-Customer: acme' -H 'X-Product: forge' -H 'X-Env: prod' -H 'X-Tenant: main' \ -d '{"filter":{"status":"shipped","region":"eu"}}'The response is the standard envelope. An empty body is legal and behaves as a list.
| Status | Code | Condition |
|---|---|---|
| 200 | — | Success |
| 400 | search_body_parse_failed | Body is not decodable JSON |
| 400 | bad_path | Entity path segment empty |
| 501 | search_text_not_supported | Body carries query — free-text search has no client wired |
| 501 | search_dsl_not_supported | filter is a string rather than an object |
| 403 / 404 / 422 / 500 | search_query_failed | Engine error |
Bulk operations envelope
Section titled “Bulk operations envelope”POST · PUT · PATCH · DELETE on /data/rest, with no entity segment. The whole batch
runs inside one transaction.
{ "operations": [ {"action": "create", "data": {"customer": {"name": "Acme"}}}, {"action": "update", "data": {"order": {"id": "01J…", "status": "shipped"}}}, {"action": "delete", "data": {"item": {"id": "01J…"}}} ]}| Field | Rule |
|---|---|
operations | Non-empty array. Results come back in input order |
action | create, update, delete, restore, upsert. Omit it to inherit the verb default |
data | Object with exactly one key — the entity name. Zero keys or two keys fail the operation |
The HTTP verb supplies the default action for operations that omit one: POST → create,
PUT and PATCH → update, DELETE → delete.
Success is 200:
{ "operations": [ { "data": [{"id": "01J…"}], "meta": {"action": "create", "entity": "customer", "count": 1, "affected": 1} } ]}Each entry is the standard envelope for that operation, so a client decodes one shape
everywhere. ?stats=true adds a top-level meta.batch block —
{operations_run, ok, fail, duration_ms} — the batch’s own counters, not the per-query stats block.
| Status | Code | Condition |
|---|---|---|
| 200 | — | Every operation committed |
| 400 | body_read | Body could not be read |
| 400 | bad_json | Envelope is not parseable |
| 400 | empty_envelope | operations empty or absent. A body that carries query or queries instead is a bulk read and is routed there |
| 403 | no_tenant | No tenant resolved |
| 422 | bulk_failed | An operation failed; everything rolled back |
| 500 | engine_unavailable | Engine could not be resolved |
What the envelope does not support
Section titled “What the envelope does not support”| Limit | Detail |
|---|---|
| Custom primary keys | update, delete, restore read the literal key id, and upsert conflicts on the literal column id. An entity with a differently-named primary key is unaddressable here — use the single-row routes, which honour the declared key |
| Multi-row payloads | Only create accepts an array of objects. update, delete, restore and upsert reject anything but a single object |
| Multi-entity operations | One entity per operation; use one operation per entity |
| Per-operation datastore | ?datastore= applies to the whole batch |
A hard delete inside the envelope is expressed per row: {"data": {"item": {"id": "…", "purge": true}}}.
A concurrency token likewise: {"data": {"order": {"id": "…", "status": "shipped", "expect": {"version": 7}}}}
on an update or delete row — expect is read and removed, never written. A mismatched token
fails the operation, and with it the batch.
Bulk read
Section titled “Bulk read”QUERY /data/rest runs one query, or a named batch of queries, in one request. The QUERY verb
is the contract. Clients and intermediaries that cannot send a custom verb use POST /data/rest
with X-HTTP-Method-Override: QUERY or ?_method=query. For now, a POST /data/rest whose body
is the read shape — query or queries, no operations — is also accepted as an alternate and
reaches the same surface; do not build on it, send QUERY. The QUERY route accepts an API key
as well as a bearer token; the POST forms need a bearer token.
{ "queries": { "pending": { "from": "track_orders", "where": { "status": "pending" } }, "recent": "track_orders | where created_at > $since | order by created_at desc | limit 20" }, "variables": { "since": "2026-09-01T00:00:00Z" }}| Field | Rule |
|---|---|
query | One query — a pipe-syntax string or a JSON object. The response is the plain envelope, as for GET /data/rest/{entity} |
queries | An object of name → query, at most 25. Names are identifiers (^[_A-Za-z][_0-9A-Za-z]*$), unique within the request, and never batch or queries |
variables | Values for $name placeholders in the queries. A placeholder with no value fails the query |
Exactly one of query and queries is required. Reads only: a mutation, a raw $expr fragment, or
a where the parser could not understand are refused rather than run.
{ "data": { "pending": [ { "id": "01J…", "status": "pending" } ], "recent": null }, "meta": { "queries": { "pending": { "entity": "track_orders", "count": 1 } }, "batch": { "queries_run": 2, "ok": 1, "fail": 1, "duration_ms": 7 } }, "errors": [ { "code": "query_failed", "message": "…", "path": ["recent"] } ]}data is keyed by query name in request order; meta.queries.<name> carries each query’s own
meta, and meta.batch the batch counters. A query that fails sets data.<name> to null and
adds an errors[] entry whose path names it, while the other queries still return their rows —
the response is 200. Only a malformed batch is a 400:
| Status | Code | Condition |
|---|---|---|
| 400 | query_missing | Neither query nor queries, or an empty queries object |
| 400 | bad_queries | queries is not a JSON object |
| 400 | too_many_queries | More than 25 queries; details.max, details.got |
| 400 | bad_query_name / reserved_query_name / duplicate_query_name | A name that is not an identifier, is batch or queries, or repeats |
| 400 | query_parse_failed / query_kind_refused / query_filter_unparsed / query_param_missing | The single query could not be parsed, is not a read, lost its filter, or names a variable that was not supplied. In the batch form the same failures are per-query errors[] entries |
| 403 | no_tenant | No tenant resolved |
Import and export
Section titled “Import and export”/data/bulk is a separate route from the operations envelope: it moves rows in and out of one
entity in a file format, and does no transaction wrapping.
| Method | Purpose | Query parameters |
|---|---|---|
POST | Import; the request body is the raw data | entity (required), format, chunk (default 1000), skiperrors, datastore |
PATCH | Export; empty request body | entity (required), format, limit, fields, orderby, filter (repeatable k=v), datastore |
format accepts csv, json, jsonl, and ndjson as an alias for jsonl. It defaults to
json. No other format is accepted.
curl -sX POST 'https://<host>/data/bulk?entity=customer&format=csv&chunk=500&skiperrors=true' \ -H 'Authorization: Bearer <token>' \ -H 'X-Customer: acme' -H 'X-Product: forge' -H 'X-Env: prod' -H 'X-Tenant: main' \ --data-binary @customers.csv{ "imported": 482, "skipped": 3, "errors": [] }curl -sX PATCH 'https://<host>/data/bulk?entity=customer&format=csv&fields=id,name&orderby=-created_at&filter=region=eu' \ -H 'Authorization: Bearer <token>' \ -H 'X-Customer: acme' -H 'X-Product: forge' -H 'X-Env: prod' -H 'X-Tenant: main' \ -o customers.csvExport sets Content-Type to text/csv; charset=utf-8 (plus Content-Disposition: attachment; filename="<entity>.csv"), application/x-ndjson; charset=utf-8, or
application/json; charset=utf-8.
| Status | Code | Condition |
|---|---|---|
| 200 | — | Success |
| 400 | missing_entity | ?entity= absent |
| 400 | bad_format | Unsupported ?format= |
| 403 | no_tenant | No tenant resolved |
| 500 | import_failed | Import failed |
Anonymous mirror
Section titled “Anonymous mirror”Every route below is served under /data/anon/rest/… with no Authorization header. The four
tenant headers are still required, and the request is still treated as external, so
access rules and
row-level security apply in full — an
anonymous caller sees exactly what the rules grant an unauthenticated principal.
| Mirrored | Not mirrored |
|---|---|
POST|PUT|PATCH|DELETE /data/anon/rest | POST /data/rest/{entity}/id/{id}/restore |
GET /data/anon/rest/{entity} | POST /data/rest/{entity}/action/{action} |
GET /data/anon/rest/{entity}/id/{id} | POST|PATCH /data/bulk |
POST /data/anon/rest/{entity} | |
PUT|PATCH /data/anon/rest/{entity} | |
PUT|PATCH /data/anon/rest/{entity}/id/{id} | |
DELETE /data/anon/rest/{entity}/id/{id} | |
POST /data/anon/rest/{entity}/search |
Worked lifecycle
Section titled “Worked lifecycle”Create, read back, update, delete, and confirm the soft delete — the sequence the block’s own functional suite runs against PostgreSQL, asserting each step in the response and directly in the database.
curl -sX POST https://<host>/data/anon/rest/currency \ -H 'Content-Type: application/json' \ -H 'X-Customer: acme' -H 'X-Product: forge' -H 'X-Env: dev' -H 'X-Tenant: dev' \ -d '{"code":"EUR","name":"Euro","symbol":"€","fraction_units":100}'201 {"data":[{"id":"01JC8…","code":"EUR",…}],"meta":{"count":1}}curl -s https://<host>/data/anon/rest/currency/id/01JC8… \ -H 'X-Customer: acme' -H 'X-Product: forge' -H 'X-Env: dev' -H 'X-Tenant: dev'200 {"data":[{"id":"01JC8…","code":"EUR",…}],"meta":{"count":1}}curl -sX PATCH https://<host>/data/anon/rest/currency/id/01JC8… \ -H 'Content-Type: application/json' \ -H 'X-Customer: acme' -H 'X-Product: forge' -H 'X-Env: dev' -H 'X-Tenant: dev' \ -d '{"name":"Euro (updated)"}'200 {"data":[{"id":"01JC8…","name":"Euro (updated)",…}],"meta":{"count":1}}curl -sX DELETE https://<host>/data/anon/rest/currency/id/01JC8… \ -H 'X-Customer: acme' -H 'X-Product: forge' -H 'X-Env: dev' -H 'X-Tenant: dev'200 delete succeeded — the row is retained with its delete markers stamped404 the same GET now returns row_not_foundContinue with
Section titled “Continue with”- GraphQL — the same entities over
POST /data/graphql - Entity metadata — schema introspection and codegen exports
- Request flags — every flag, its spellings, and which ones are inert
- Query DSL — the operator grammar the query string cannot reach
- Error codes — the complete code table