Create or replace one 'owners' row
const url = 'http://localhost:8080/api/owners/2489E9AD-2EE2-8E00-8EC9-32D5F69181C0';const options = { method: 'PUT', headers: {'Content-Type': 'application/json'}, body: '{"name":"example","email":"hello@example.com","phone":"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/owners/2489E9AD-2EE2-8E00-8EC9-32D5F69181C0 \ --header 'Content-Type: application/json' \ --data '{ "name": "example", "email": "hello@example.com", "phone": "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.
If-Match conditions the write on the row’s current version, compared with RFC 9110 §13.1.1’s strong comparison. Send back one ETag exactly as a previous response returned it, or * to require only that the row still exist. Anything this API cannot turn into exactly one version it minted — several tags, a weak W/ tag, an opaque value it never issued — is 412, never ignored. If-None-Match is refused with 412 as well: this API compares a row against the version a caller holds, and has no channel for “act only if the row is not at this version”. That is a labelled deviation from RFC 9110 §13.1.2, which would let a non-matching If-None-Match simply succeed — Alvo cannot evaluate the header at all, so a conforming success would be indistinguishable from a precondition that was never checked.
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”Perform the write only if the row is still at this version. One entity tag exactly as a previous response returned it, or * to require only that the row still exist. Anything else — several tags, a weak W/ tag, a value this API never minted — is 412 rather than ignored, because ignoring a precondition is the lost update the header exists to prevent.
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
{ "name": "example", "email": "hello@example.com", "phone": "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 person or company a vehicle is registered to.
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", "email": "someone@example.com", "phone": "+421 900 123 456", "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"The created row. ‘Location’ names it, and ‘ETag’ carries the version a later conditional write may send as ‘If-Match’ — so a first conditional write needs no read of its own.
A person or company a vehicle is registered to.
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", "email": "someone@example.com", "phone": "+421 900 123 456", "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"The 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 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