Create or replace one 'inspections' row
const url = 'http://localhost:8080/api/inspections/2489E9AD-2EE2-8E00-8EC9-32D5F69181C0';const options = { method: 'PUT', headers: {'Content-Type': 'application/json'}, body: '{"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 PUT \ --url http://localhost:8080/api/inspections/2489E9AD-2EE2-8E00-8EC9-32D5F69181C0 \ --header 'Content-Type: application/json' \ --data '{ "vehicle_id": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0", "inspector_name": "example", "inspected_on": "2026-04-15", "passed": true, "notes": "example" }'Creates or replaces the row this path names, and returns it. A row that did not exist is created under the id in the path and answers 201 with a Location; one that did is replaced and answers 200.
The row is written whole. A field the body does not mention is written null rather than left at its stored value — that is the difference from PATCH on this same path, and it is why a body that omits a required field is refused with 422 naming the field rather than treated as a partial write. A field that is both required and hidden cannot be restated by a caller who cannot read it, which makes such an entity reachable only through PATCH for them.
The path is the only place id may appear. A body naming id is refused exactly as it is everywhere else, and so is tenant_id: on a tenant-scoped entity a created row lands in the caller’s own tenant, and a caller creating into another tenant uses POST on the collection.
The caller needs both create and update. Which branch runs depends on stored data, so requiring only the branch’s own operation would make the permission you need depend on whether the row happens to exist. An id already held by a row this caller cannot see answers 409: a primary key cannot collide silently, and the alternative would be writing over a row the caller’s policy excludes.
This entity’s rows carry no version, so this write cannot be conditioned. A row version comes from audit: true in the descriptor; without it there is no column the framework writes on every change, and a version a caller can rewrite is not a version. No ETag is ever returned for a row of this entity, so there is no tag a caller could ever send back — and an If-Match naming a version is therefore refused with 412, never ignored, because a success would tell a caller their condition held when nothing was compared. If-None-Match is refused with 412 here as it is on every write. The one precondition that is accepted is If-Match: *, and it changes nothing: it asks only that the row still exist, which an absent row already answers with 404. Neither header is offered as a parameter on this operation for that reason — a parameter is an invitation to send a value, and there is no value to send. Note what this means for a client that blanket-attaches If-Match to every mutating request, as several SDKs do: unless it sends *, it is refused here. Add audit: true to the entity to make a conditional write possible.
Idempotency-Key makes the retry answer the row, and a replay always answers 200 — never 201, and with no Location. A 201 reports that this request created the row, and a replay performs no act at all: it reports the state the first request left.
Authorizations
Section titled “Authorizations”- None
- alvoApiKey
Parameters
Section titled “Parameters”Path Parameters
Section titled “Path Parameters”The row’s key. A value routing cannot read as a GUID is a 404 from routing itself, before the endpoint runs; a well-formed key naming a row the caller may not see is also a 404, indistinguishable from one that does not exist.
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 whole row, as the entity’s declared fields. A field this body omits is written null rather than left at its stored value, so every required field must be present.
The whole row. A field this object does not mention is written null rather than left at its stored value, so every field the descriptor declares required must be present. The row’s id comes from the path, and the framework’s own columns — tenant_id included — are refused if supplied: a created row lands in the caller’s own tenant.
object
Example generated
{ "vehicle_id": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0", "inspector_name": "example", "inspected_on": "2026-04-15", "passed": true, "notes": "example"}Responses
Section titled “Responses”The row as it now stands, when this request replaced an existing one or replayed an ‘Idempotency-Key’ a previous request spent.
A roadworthiness inspection performed on a vehicle.
One row as a single read, a create or an update returns it. Every field is present, even one with no value — that field is present and null. Fields marked read-only are written by the framework and refused in a request body.
object
Example
{ "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-storeThe created row. ‘Location’ names it. This entity’s rows carry no version, so no ‘ETag’ is returned and no later write can be conditioned on one.
A roadworthiness inspection performed on a vehicle.
One row as a single read, a create or an update returns it. Every field is present, even one with no value — that field is present and null. Fields marked read-only are written by the framework and refused in a request body.
object
Example
{ "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-storeThe path of the created row, under the same route prefix the create was sent to.
A 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 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 was supplied that this entity cannot answer. Its rows carry no version — ‘audit: true’ is what mints one — so an ‘If-Match’ naming a version, and any ‘If-None-Match’, are refused here rather than ignored: there is nothing stored for either to be compared against. On this entity the status never means ‘the version did not match’, because no comparison is possible. ‘If-Match: *’ is accepted and is not this refusal.
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