Query 'vehicles' rows through a request body
const url = 'http://localhost:8080/api/vehicles/query';const options = { method: 'POST', headers: {Prefer: 'count=exact', 'Content-Type': 'application/json'}, body: '{"select":"example","order":"example","limit":50,"offset":1,"after":"example","or":["example"],"and":["example"],"id":["example"],"vin":["example"],"plate":["example"],"make":["example"],"model":["example"],"year":["example"],"color":["example"],"owner_id":["example"],"created_at":["example"],"created_by":["example"],"updated_at":["example"],"updated_by":["example"]}'};
try { const response = await fetch(url, options); const data = await response.json(); console.log(data);} catch (error) { console.error(error);}curl --request POST \ --url http://localhost:8080/api/vehicles/query \ --header 'Content-Type: application/json' \ --header 'Prefer: count=exact' \ --data '{ "select": "example", "order": "example", "limit": 50, "offset": 1, "after": "example", "or": [ "example" ], "and": [ "example" ], "id": [ "example" ], "vin": [ "example" ], "plate": [ "example" ], "make": [ "example" ], "model": [ "example" ], "year": [ "example" ], "color": [ "example" ], "owner_id": [ "example" ], "created_at": [ "example" ], "created_by": [ "example" ], "updated_at": [ "example" ], "updated_by": [ "example" ] }'Reads a page of rows the caller’s policy admits, taking the same parameters in a JSON request body.
It exists for one reason: a filter a request line cannot carry. Alvo’s own budgets are generous — 256 filter terms and 1000 in candidates — and a proxy’s URL limit is reached first, so ?id=in.(…400 ids…) is refused by an intermediary with a 414 carrying no violations array at all. Sent as a body it is answered normally.
The body is a JSON object whose members are the query parameters, and the grammar inside each value is exactly the one the query string carries: {"year": "gte.2020", "or": ["(color.eq.red,color.eq.blue)"], "select": "id,label:make", "limit": 50}. A repeated parameter is an array of strings; the same name twice in one object is refused, because JSON leaves the order of two such members undefined. {} is the empty query — every readable field, the default page.
Values are not percent-encoded here, and that is the point. A query string carries the escaping of a value; a JSON string carries the value. So {"make": "like.100%"} is what ?make=like.100%25 means, and + is a plus rather than a space. Everything else is identical: the same parser, the same refusals, the same page envelope, the same Prefer: count preference.
This is a read and is gated as list. A caller whose list is unconfigured is refused here exactly as on the collection GET, before the body is read at all — so a refusal never arrives dressed as a complaint about the body.
Idempotency-Key is accepted and ignored. There is nothing to make idempotent: no row is written, so a retry costs a second read and nothing else. It is accepted rather than refused because several SDKs attach it to every POST.
A refusal’s pointer tells you where to look: an empty string or one beginning with / is a JSON Pointer into this body, and any other value is the role of a query parameter — filter, order, limit, offset, after or select.
Filtering follows PostgREST’s spelling: every query parameter that is not one of the reserved names (order, limit, offset, after, select, or, and, not) names a field, and its value is <operator>.<operand> — ?year=gte.2020. The operators are eq, neq, gt, gte, lt, lte, like, ilike, in, is. in takes a bracketed candidate list (in.(skoda,vw)) and is takes exactly null, true or false; like/ilike take a pattern. An unrecognised operator is refused with 422, never quietly read as equality, and an ordering operator applied to a type this API defines no total order over is refused too — so one filter means the same thing on every engine.
Several parameters are one conjunction (AND), which is the only reading that keeps a filter narrowing as terms are added. or=(…) and and=(…) group terms explicitly and may nest, and prefixing any parameter name or group keyword with not. negates it (not.color=eq.red, not.or=(…)).
A field that is unavailable to the caller is refused exactly like one that does not exist, in filter, order and select alike — the refusal names the parameter’s role and never the field, so filtering, ordering or selecting cannot be used to discover a hidden field’s name. The one exception is a field the descriptor also marks required: its name is published in the write schemas below, because a mandatory field a caller cannot see could not be supplied — see the overview.
Authorizations
Section titled “Authorizations”- None
- alvoApiKey
Parameters
Section titled “Parameters”Header Parameters
Section titled “Header Parameters”count=exact fills the page envelope’s count with the number of rows the query matches in total. Opt-in, because it is a second scan of the matching set on every request; a request sending no recognised count preference gets null there.
count=planned and count=estimated are accepted and degrade to an exact count, so they fill count too — a planner estimate exists on one supported engine and not the other, and this API answers identically on both. The response says which was applied in Preference-Applied, and it is always count=exact. Per RFC 7240 a preference this server does not recognise is ignored rather than refused, and its absence from Preference-Applied is how that is reported.
Example
count=exactRequest Bodyrequired
Section titled “Request Bodyrequired”The query parameters, as an object. An empty object reads the first page with no filter.
The query parameters, as an object. A member’s name is a parameter and its value is the text a query string would carry; an array repeats the parameter. See the operation description for the grammar and for what a field property accepts.
object
Responses
Section titled “Responses”A page of rows the caller’s policy admits.
One page of rows, the cursor that reads the page after it, and — when the request opted in with a Prefer: count preference — how many rows the query matches in total.
object
The rows in this page, in the requested order.
A registered vehicle, owned by exactly one owner.
One row inside a list’s page. Every field is present unless the request narrowed the projection with select; a field with no value is present and null. Fields marked read-only are written by the framework and refused in a request body.
object
The opaque cursor for the next page, or null when this page is the last. Send it back verbatim as after; it is the provider’s to interpret and must not be decoded.
How many rows the query matches in total — not the size of this page — or null unless the request sent a recognised Prefer: count preference — exact, or planned/estimated, which degrade to an exact count. It is narrowed by the caller’s policy and by the filter, and not by limit, offset or after, so it does not shrink as you page. Opt-in because an exact count is a second scan of the matching set on every request. Exact means “not an estimate”, not “consistent with items”: it is taken in a second statement, so a write landing between the two can make it differ from the rows by one.
Example
{ "items": [ { "id": "3f8d6c1e-9b47-4a5f-8c21-0d7e5a2b6f04", "owner_id": "3f8d6c1e-9b47-4a5f-8c21-0d7e5a2b6f04", "created_at": "2026-01-31T09:30:00+00:00", "created_by": "3f8d6c1e-9b47-4a5f-8c21-0d7e5a2b6f04", "updated_at": "2026-01-31T09:30:00+00:00", "updated_by": "3f8d6c1e-9b47-4a5f-8c21-0d7e5a2b6f04" } ]}Headers
Section titled “Headers”Always no-store. These representations are policy-masked per caller and the ETag is minted over the row’s version rather than over the response bytes, so no cache — shared or private — may keep the body. Keeping the tag alone is not caching a representation, which is what makes a conditional write still possible.
Example
no-storeThe row’s version as a strong entity tag. Send it back verbatim as If-Match to make a later write conditional; it is compared octet-for-octet, so it must not be reformatted. Absent when the row has no version this caller can be given.
Example
"638712345678900000"What was done with the request’s Prefer header (RFC 7240 §3). Present only when a preference was applied, and always count=exact — count=planned and count=estimated degrade to a real count, and this is where a caller who asked for an estimate learns they received the exact one. Absent means no preference was applied, which is how RFC 7240 reports one this server does not recognise.
Example
count=exactA credential was presented and cannot be used — unknown, revoked, expired, malformed, or issued for another tenant. One wording for all of them, so key ids cannot be enumerated one request at a time. ‘WWW-Authenticate’ names the scheme and the header to send. Presenting no credential at all is not a 401: an anonymous caller is judged by policy like any other.
An RFC 9457 problem document. Branch on type; detail is prose and, per §3.1.1, ought not be parsed. A type keys on the kind of refusal and never on its reason — one value covers every policy refusal, and one covers both an absent row and a row the caller may not see.
object
The classification to branch on. Every value is listed here, so a client needs no prose: the namespace is https://alvo.dev/errors/ and the slug names the kind of refusal.
The status code’s standard reason phrase. Carries no Alvo-specific information.
The HTTP status code, repeated in the body as RFC 9457 §3.1.2 allows.
What went wrong, in prose. Built from constants and server-owned values only — it never echoes a caller-supplied field name or value, because a refusal is answered before authorization on some paths and would otherwise be the cheapest oracle in the API.
Every itemised reason the request was refused, not only the first — so one round trip is enough to repair it. Present on a refusal that has itemised reasons (a malformed query string, a body the entity’s declared shape refuses) and absent on one that does not.
One machine-readable reason a request was refused.
object
An RFC 6901 JSON Pointer into the request body, or the role of the query-string parameter concerned (filter, order, limit, offset, after, select). A role rather than a name, because in this filter grammar a parameter’s name is a field name.
A stable kebab-case code for the kind of violation, safe to branch on.
One sentence, free of caller-supplied text.
What to change. Every refusal Alvo itself raises carries one; it is nullable for a violation forwarded from a source that has none to offer, where an empty string would be indistinguishable from a blank suggestion.
Example
{ "type": "https://alvo.dev/errors/validation", "violations": [ { "pointer": "/name", "code": "max-length" } ]}Headers
Section titled “Headers”Always no-store. These representations are policy-masked per caller and the ETag is minted over the row’s version rather than over the response bytes, so no cache — shared or private — may keep the body. Keeping the tag alone is not caching a representation, which is what makes a conditional write still possible.
Example
no-storeThe RFC 7235 challenge, naming the scheme and the request header a credential is read from — so an agent can discover how to authenticate rather than guess. The header name is host configuration, which is why the challenge states it rather than assuming a default.
The operation is refused. Two kinds, told apart by the problem ‘type’ and never by its prose: ‘out-of-scope’ means the presented key’s scopes do not cover this entity and operation (grant the key the scope), and ‘forbidden’ means policy refused it (change a rule). A policy refusal here is an operation-level one — the operation is unconfigured, the entity is unknown to the applied descriptor, the caller has no tenant on a tenant-scoped entity, or the policy reads a caller value this caller does not carry. It is never ‘your rule excluded these rows’; see the 200.
An RFC 9457 problem document. Branch on type; detail is prose and, per §3.1.1, ought not be parsed. A type keys on the kind of refusal and never on its reason — one value covers every policy refusal, and one covers both an absent row and a row the caller may not see.
object
The classification to branch on. Every value is listed here, so a client needs no prose: the namespace is https://alvo.dev/errors/ and the slug names the kind of refusal.
The status code’s standard reason phrase. Carries no Alvo-specific information.
The HTTP status code, repeated in the body as RFC 9457 §3.1.2 allows.
What went wrong, in prose. Built from constants and server-owned values only — it never echoes a caller-supplied field name or value, because a refusal is answered before authorization on some paths and would otherwise be the cheapest oracle in the API.
Every itemised reason the request was refused, not only the first — so one round trip is enough to repair it. Present on a refusal that has itemised reasons (a malformed query string, a body the entity’s declared shape refuses) and absent on one that does not.
One machine-readable reason a request was refused.
object
An RFC 6901 JSON Pointer into the request body, or the role of the query-string parameter concerned (filter, order, limit, offset, after, select). A role rather than a name, because in this filter grammar a parameter’s name is a field name.
A stable kebab-case code for the kind of violation, safe to branch on.
One sentence, free of caller-supplied text.
What to change. Every refusal Alvo itself raises carries one; it is nullable for a violation forwarded from a source that has none to offer, where an empty string would be indistinguishable from a blank suggestion.
Example
{ "type": "https://alvo.dev/errors/validation", "violations": [ { "pointer": "/name", "code": "max-length" } ]}Headers
Section titled “Headers”Always no-store. These representations are policy-masked per caller and the ETag is minted over the row’s version rather than over the response bytes, so no cache — shared or private — may keep the body. Keeping the tag alone is not caching a representation, which is what makes a conditional write still possible.
Example
no-storeThe request body was not declared as JSON, or carried no ‘Content-Type’ at all. Send ‘Content-Type: application/json’, or any ‘application/*+json’; a POST refusal also carries ‘Accept-Post’ and a PATCH refusal ‘Accept-Patch’ naming what the operation accepts. The requirement exists because a body-taking route with no media-type requirement is reachable as a CORS simple request, and a host with its own cross-site-request-forgery defence can turn it off.
An RFC 9457 problem document. Branch on type; detail is prose and, per §3.1.1, ought not be parsed. A type keys on the kind of refusal and never on its reason — one value covers every policy refusal, and one covers both an absent row and a row the caller may not see.
object
The classification to branch on. Every value is listed here, so a client needs no prose: the namespace is https://alvo.dev/errors/ and the slug names the kind of refusal.
The status code’s standard reason phrase. Carries no Alvo-specific information.
The HTTP status code, repeated in the body as RFC 9457 §3.1.2 allows.
What went wrong, in prose. Built from constants and server-owned values only — it never echoes a caller-supplied field name or value, because a refusal is answered before authorization on some paths and would otherwise be the cheapest oracle in the API.
Every itemised reason the request was refused, not only the first — so one round trip is enough to repair it. Present on a refusal that has itemised reasons (a malformed query string, a body the entity’s declared shape refuses) and absent on one that does not.
One machine-readable reason a request was refused.
object
An RFC 6901 JSON Pointer into the request body, or the role of the query-string parameter concerned (filter, order, limit, offset, after, select). A role rather than a name, because in this filter grammar a parameter’s name is a field name.
A stable kebab-case code for the kind of violation, safe to branch on.
One sentence, free of caller-supplied text.
What to change. Every refusal Alvo itself raises carries one; it is nullable for a violation forwarded from a source that has none to offer, where an empty string would be indistinguishable from a blank suggestion.
Example
{ "type": "https://alvo.dev/errors/validation", "violations": [ { "pointer": "/name", "code": "max-length" } ]}Headers
Section titled “Headers”Always no-store. These representations are policy-masked per caller and the ETag is minted over the row’s version rather than over the response bytes, so no cache — shared or private — may keep the body. Keeping the tag alone is not caching a representation, which is what makes a conditional write still possible.
Example
no-storeThe request could not be acted on: a query string or body that is malformed, a body the entity’s declared shape refuses, or a header this API cannot honour. The ‘violations’ array carries every reason at once — a pointer, a stable code and a fix suggestion each — never only the first, so one round trip is enough to repair the request.
An RFC 9457 problem document. Branch on type; detail is prose and, per §3.1.1, ought not be parsed. A type keys on the kind of refusal and never on its reason — one value covers every policy refusal, and one covers both an absent row and a row the caller may not see.
object
The classification to branch on. Every value is listed here, so a client needs no prose: the namespace is https://alvo.dev/errors/ and the slug names the kind of refusal.
The status code’s standard reason phrase. Carries no Alvo-specific information.
The HTTP status code, repeated in the body as RFC 9457 §3.1.2 allows.
What went wrong, in prose. Built from constants and server-owned values only — it never echoes a caller-supplied field name or value, because a refusal is answered before authorization on some paths and would otherwise be the cheapest oracle in the API.
Every itemised reason the request was refused, not only the first — so one round trip is enough to repair it. Present on a refusal that has itemised reasons (a malformed query string, a body the entity’s declared shape refuses) and absent on one that does not.
One machine-readable reason a request was refused.
object
An RFC 6901 JSON Pointer into the request body, or the role of the query-string parameter concerned (filter, order, limit, offset, after, select). A role rather than a name, because in this filter grammar a parameter’s name is a field name.
A stable kebab-case code for the kind of violation, safe to branch on.
One sentence, free of caller-supplied text.
What to change. Every refusal Alvo itself raises carries one; it is nullable for a violation forwarded from a source that has none to offer, where an empty string would be indistinguishable from a blank suggestion.
Example
{ "type": "https://alvo.dev/errors/validation", "violations": [ { "pointer": "/name", "code": "max-length" } ]}Headers
Section titled “Headers”Always no-store. These representations are policy-masked per caller and the ETag is minted over the row’s version rather than over the response bytes, so no cache — shared or private — may keep the body. Keeping the tag alone is not caching a representation, which is what makes a conditional write still possible.
Example
no-store