Update many 'inspections' rows in one transaction
const url = 'http://localhost:8080/api/inspections/batch';const options = { method: 'PATCH', headers: {'Content-Type': 'application/json'}, body: '{"rows":[{"id":"2489E9AD-2EE2-8E00-8EC9-32D5F69181C0","vehicle_id":"2489E9AD-2EE2-8E00-8EC9-32D5F69181C0","inspector_name":"example","inspected_on":"2026-04-15","passed":true,"notes":"example"}]}'};
try { const response = await fetch(url, options); const data = await response.json(); console.log(data);} catch (error) { console.error(error);}curl --request PATCH \ --url http://localhost:8080/api/inspections/batch \ --header 'Content-Type: application/json' \ --data '{ "rows": [ { "id": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0", "vehicle_id": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0", "inspector_name": "example", "inspected_on": "2026-04-15", "passed": true, "notes": "example" } ] }'Updates many rows in one transaction. Send {"rows": [ … ]}, each element an object carrying the row’s id plus the fields to change on it — partial, exactly as the single-row update is. There is no If-Match here: one version cannot condition many rows, and accepting one would check a single row while appearing to check all of them.
The batch is one transaction: every row is written, or none is. A refusal on the last row leaves the first unwritten, so a caller repairs the rows the response names and resends the whole batch. There is no partial outcome to reconcile.
Every row is judged individually against your own policy — the WITH CHECK predicate and, on a tenant-scoped entity, the tenant scope — exactly as the single-row route judges one. A batch is not a way to write rows a single call could not.
Every offending row is reported, not the first. Each entry of violations carries a /rows/{index} pointer, so a five-hundred-row import is repaired in one round trip rather than five hundred. A row you cannot see and a row that does not exist are the same refusal, deliberately: telling them apart would let one request ask as many existence questions as it carries rows.
A 409 names the field and no row index. A unique value is something you can guess, so an index would turn one collision probe into as many per request as the batch carries rows.
Idempotency-Key covers the whole batch, because a batch is one request and a partial retry is not expressible. The same key with a different list of rows is a 409, not a replay.
Authorizations
Section titled “Authorizations”- None
- alvoApiKey
Parameters
Section titled “Parameters”Header Parameters
Section titled “Header Parameters”Makes this write retry-safe. The result is recorded against the key and the caller’s own scope, so the same key repeated replays the first result and writes nothing further: a retried create is the first row, a retried update is the row, and a retried delete is 204 rather than a 404 the caller cannot tell from somebody else’s delete. The key covers the whole request — the method, the entity, the row it addresses, the If-Match it carries and the body — so the same key against another row, or with another If-Match, is 409 rather than a replay. An anonymous caller’s key is refused, because every anonymous caller shares one identity and their keys would share one space. The bound below is a byte bound — at most 255 bytes once UTF-8 encoded — so a key of non-ASCII characters reaches it sooner than maxLength suggests; an over-long key is refused rather than shortened, because two keys differing only past the cut would become one.
Request Bodyrequired
Section titled “Request Bodyrequired”The rows to write, under a ‘rows’ array. A batch is one transaction: every row is written, or none is.
The rows to write, in one transaction.
object
One element per row, in the order you want them reported.
One row of a batch update: which row, and the fields to change on it.
object
The row to change, exactly as a previous response returned it.
Example generated
{ "rows": [ { "id": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0", "vehicle_id": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0", "inspector_name": "example", "inspected_on": "2026-04-15", "passed": true, "notes": "example" } ]}Responses
Section titled “Responses”Every row the batch wrote, in request order, and how many it affected. A batch delete answers an empty ‘items’ with a non-zero ‘affected’.
What the batch wrote. A batch is one transaction, so this is never a partial outcome.
object
The rows the batch wrote, in the order you sent them. Empty for a batch delete, which produces no rows — read affected there.
A roadworthiness inspection performed on a vehicle.
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
How many rows the batch wrote or removed.
Example
{ "items": [ { "id": "3f8d6c1e-9b47-4a5f-8c21-0d7e5a2b6f04", "vehicle_id": "3f8d6c1e-9b47-4a5f-8c21-0d7e5a2b6f04", "inspected_on": "2026-01-31" } ]}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-storeA 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, or one or more rows are — and on this route a policy refusal CAN be per row, unlike every other operation. Each entry of ‘violations’ carries a ‘/rows/{index}’ pointer naming a row policy refused: its ‘WITH CHECK’ predicate, the tenant scope, a row that is not yours or does not exist (one refusal for both, so a batch cannot be used to ask which), or a row the batch named twice. A batch is one transaction, so nothing was written — repair the rows the response names and resend the whole batch. A refusal the entity’s declared SHAPE produced is a 422 instead, and carries the same pointers. The two operation-level kinds are unchanged and are still told apart by the problem ‘type’: ‘out-of-scope’ means the presented key’s scopes do not cover this entity and operation (grant the key the scope), ‘forbidden’ means policy refused (change a rule, or a row).
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 conflicts with what is already stored. Two kinds, told apart by the problem ‘type’: ‘idempotency-conflict’ means the ‘Idempotency-Key’ was already used by this caller for a different request — a different body, but also a different row or a different ‘If-Match’, because the key covers the whole request (retry the identical request to replay its result, or send a fresh key); ‘conflict’ means a constraint the database enforces refused the write — a value another record already holds on a field declared unique, or a delete another record still references through a ‘ref’ declaring onDelete: restrict. The ‘violations’ array names the field for the first of those and carries a fix suggestion for both.
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-storeA precondition this API cannot evaluate, or one that did not hold. A header it cannot compare is refused rather than ignored: ignoring it would be the lost update ‘If-Match’ exists to prevent, and the caller would read the success as proof it did not happen.
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