Skip to content
Alvo is pre-v0.1: the image runs from its edge tag, and no NuGet package or release is published yet.Pre-v0.1: no release yet.Roadmap and status

Create or replace one 'vehicles' row

PUT
/api/vehicles/{id}
curl --request PUT \
--url http://localhost:8080/api/vehicles/2489E9AD-2EE2-8E00-8EC9-32D5F69181C0 \
--header 'Content-Type: application/json' \
--data '{ "vin": "example", "plate": "example", "make": "example", "model": "example", "year": 1, "color": "example", "owner_id": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0" }'

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.

id
required
string format: uuid

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.

If-Match
string

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.

Idempotency-Key
string
>= 1 characters <= 255 characters

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.

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.

Media typeapplication/json
vehiclesReplace

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
vin
required
string
<= 17 characters
plate
required
string
<= 12 characters
make
required
string
<= 60 characters
model
required
string
<= 60 characters
year
required
integer format: int64
color
null | string
<= 30 characters
owner_id
required
string format: uuid

Example generated

{
"vin": "example",
"plate": "example",
"make": "example",
"model": "example",
"year": 1,
"color": "example",
"owner_id": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0"
}

The row as it now stands, when this request replaced an existing one or replayed an ‘Idempotency-Key’ a previous request spent.

Media typeapplication/json
vehicles

A registered vehicle, owned by exactly one owner.

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
id
required
string format: uuid
vin
required
string
<= 17 characters
plate
required
string
<= 12 characters
make
required
string
<= 60 characters
model
required
string
<= 60 characters
year
required
integer format: int64
color
required
null | string
<= 30 characters
owner_id
required
string format: uuid
created_at
required
string format: date-time
created_by
required
null | string format: uuid
updated_at
required
string format: date-time
updated_by
required
null | string format: uuid

Example

{
"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"
}
Cache-Control
string

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
ETag
string

The 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.

Media typeapplication/json
vehicles

A registered vehicle, owned by exactly one owner.

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
id
required
string format: uuid
vin
required
string
<= 17 characters
plate
required
string
<= 12 characters
make
required
string
<= 60 characters
model
required
string
<= 60 characters
year
required
integer format: int64
color
required
null | string
<= 30 characters
owner_id
required
string format: uuid
created_at
required
string format: date-time
created_by
required
null | string format: uuid
updated_at
required
string format: date-time
updated_by
required
null | string format: uuid

Example

{
"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"
}
Cache-Control
string

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
ETag
string

The 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"
Location
string

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.

Media typeapplication/problem+json
problemDetails

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
type
required

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.

string format: uri
Allowed values: https://alvo.dev/errors/validation https://alvo.dev/errors/malformed-query https://alvo.dev/errors/forbidden https://alvo.dev/errors/out-of-scope https://alvo.dev/errors/not-found https://alvo.dev/errors/precondition-failed https://alvo.dev/errors/precondition-required https://alvo.dev/errors/idempotency-conflict https://alvo.dev/errors/conflict https://alvo.dev/errors/destructive-change https://alvo.dev/errors/unauthenticated https://alvo.dev/errors/unreadable-request https://alvo.dev/errors/unsupported-media-type https://alvo.dev/errors/internal https://alvo.dev/errors/function-failed
title
required

The status code’s standard reason phrase. Carries no Alvo-specific information.

string
status
required

The HTTP status code, repeated in the body as RFC 9457 §3.1.2 allows.

integer format: int32
detail
required

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.

string
violations

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.

Array<object>
problemViolation

One machine-readable reason a request was refused.

object
pointer
required

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.

string
code
required

A stable kebab-case code for the kind of violation, safe to branch on.

string
message
required

One sentence, free of caller-supplied text.

string
fixSuggestion

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.

null | string

Example

{
"type": "https://alvo.dev/errors/validation",
"violations": [
{
"pointer": "/name",
"code": "max-length"
}
]
}
Cache-Control
string

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
WWW-Authenticate
string

The 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.

Media typeapplication/problem+json
problemDetails

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
type
required

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.

string format: uri
Allowed values: https://alvo.dev/errors/validation https://alvo.dev/errors/malformed-query https://alvo.dev/errors/forbidden https://alvo.dev/errors/out-of-scope https://alvo.dev/errors/not-found https://alvo.dev/errors/precondition-failed https://alvo.dev/errors/precondition-required https://alvo.dev/errors/idempotency-conflict https://alvo.dev/errors/conflict https://alvo.dev/errors/destructive-change https://alvo.dev/errors/unauthenticated https://alvo.dev/errors/unreadable-request https://alvo.dev/errors/unsupported-media-type https://alvo.dev/errors/internal https://alvo.dev/errors/function-failed
title
required

The status code’s standard reason phrase. Carries no Alvo-specific information.

string
status
required

The HTTP status code, repeated in the body as RFC 9457 §3.1.2 allows.

integer format: int32
detail
required

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.

string
violations

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.

Array<object>
problemViolation

One machine-readable reason a request was refused.

object
pointer
required

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.

string
code
required

A stable kebab-case code for the kind of violation, safe to branch on.

string
message
required

One sentence, free of caller-supplied text.

string
fixSuggestion

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.

null | string

Example

{
"type": "https://alvo.dev/errors/validation",
"violations": [
{
"pointer": "/name",
"code": "max-length"
}
]
}
Cache-Control
string

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

The 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.

Media typeapplication/problem+json
problemDetails

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
type
required

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.

string format: uri
Allowed values: https://alvo.dev/errors/validation https://alvo.dev/errors/malformed-query https://alvo.dev/errors/forbidden https://alvo.dev/errors/out-of-scope https://alvo.dev/errors/not-found https://alvo.dev/errors/precondition-failed https://alvo.dev/errors/precondition-required https://alvo.dev/errors/idempotency-conflict https://alvo.dev/errors/conflict https://alvo.dev/errors/destructive-change https://alvo.dev/errors/unauthenticated https://alvo.dev/errors/unreadable-request https://alvo.dev/errors/unsupported-media-type https://alvo.dev/errors/internal https://alvo.dev/errors/function-failed
title
required

The status code’s standard reason phrase. Carries no Alvo-specific information.

string
status
required

The HTTP status code, repeated in the body as RFC 9457 §3.1.2 allows.

integer format: int32
detail
required

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.

string
violations

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.

Array<object>
problemViolation

One machine-readable reason a request was refused.

object
pointer
required

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.

string
code
required

A stable kebab-case code for the kind of violation, safe to branch on.

string
message
required

One sentence, free of caller-supplied text.

string
fixSuggestion

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.

null | string

Example

{
"type": "https://alvo.dev/errors/validation",
"violations": [
{
"pointer": "/name",
"code": "max-length"
}
]
}
Cache-Control
string

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

A 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.

Media typeapplication/problem+json
problemDetails

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
type
required

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.

string format: uri
Allowed values: https://alvo.dev/errors/validation https://alvo.dev/errors/malformed-query https://alvo.dev/errors/forbidden https://alvo.dev/errors/out-of-scope https://alvo.dev/errors/not-found https://alvo.dev/errors/precondition-failed https://alvo.dev/errors/precondition-required https://alvo.dev/errors/idempotency-conflict https://alvo.dev/errors/conflict https://alvo.dev/errors/destructive-change https://alvo.dev/errors/unauthenticated https://alvo.dev/errors/unreadable-request https://alvo.dev/errors/unsupported-media-type https://alvo.dev/errors/internal https://alvo.dev/errors/function-failed
title
required

The status code’s standard reason phrase. Carries no Alvo-specific information.

string
status
required

The HTTP status code, repeated in the body as RFC 9457 §3.1.2 allows.

integer format: int32
detail
required

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.

string
violations

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.

Array<object>
problemViolation

One machine-readable reason a request was refused.

object
pointer
required

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.

string
code
required

A stable kebab-case code for the kind of violation, safe to branch on.

string
message
required

One sentence, free of caller-supplied text.

string
fixSuggestion

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.

null | string

Example

{
"type": "https://alvo.dev/errors/validation",
"violations": [
{
"pointer": "/name",
"code": "max-length"
}
]
}
Cache-Control
string

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

The 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.

Media typeapplication/problem+json
problemDetails

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
type
required

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.

string format: uri
Allowed values: https://alvo.dev/errors/validation https://alvo.dev/errors/malformed-query https://alvo.dev/errors/forbidden https://alvo.dev/errors/out-of-scope https://alvo.dev/errors/not-found https://alvo.dev/errors/precondition-failed https://alvo.dev/errors/precondition-required https://alvo.dev/errors/idempotency-conflict https://alvo.dev/errors/conflict https://alvo.dev/errors/destructive-change https://alvo.dev/errors/unauthenticated https://alvo.dev/errors/unreadable-request https://alvo.dev/errors/unsupported-media-type https://alvo.dev/errors/internal https://alvo.dev/errors/function-failed
title
required

The status code’s standard reason phrase. Carries no Alvo-specific information.

string
status
required

The HTTP status code, repeated in the body as RFC 9457 §3.1.2 allows.

integer format: int32
detail
required

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.

string
violations

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.

Array<object>
problemViolation

One machine-readable reason a request was refused.

object
pointer
required

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.

string
code
required

A stable kebab-case code for the kind of violation, safe to branch on.

string
message
required

One sentence, free of caller-supplied text.

string
fixSuggestion

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.

null | string

Example

{
"type": "https://alvo.dev/errors/validation",
"violations": [
{
"pointer": "/name",
"code": "max-length"
}
]
}
Cache-Control
string

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

The 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.

Media typeapplication/problem+json
problemDetails

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
type
required

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.

string format: uri
Allowed values: https://alvo.dev/errors/validation https://alvo.dev/errors/malformed-query https://alvo.dev/errors/forbidden https://alvo.dev/errors/out-of-scope https://alvo.dev/errors/not-found https://alvo.dev/errors/precondition-failed https://alvo.dev/errors/precondition-required https://alvo.dev/errors/idempotency-conflict https://alvo.dev/errors/conflict https://alvo.dev/errors/destructive-change https://alvo.dev/errors/unauthenticated https://alvo.dev/errors/unreadable-request https://alvo.dev/errors/unsupported-media-type https://alvo.dev/errors/internal https://alvo.dev/errors/function-failed
title
required

The status code’s standard reason phrase. Carries no Alvo-specific information.

string
status
required

The HTTP status code, repeated in the body as RFC 9457 §3.1.2 allows.

integer format: int32
detail
required

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.

string
violations

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.

Array<object>
problemViolation

One machine-readable reason a request was refused.

object
pointer
required

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.

string
code
required

A stable kebab-case code for the kind of violation, safe to branch on.

string
message
required

One sentence, free of caller-supplied text.

string
fixSuggestion

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.

null | string

Example

{
"type": "https://alvo.dev/errors/validation",
"violations": [
{
"pointer": "/name",
"code": "max-length"
}
]
}
Cache-Control
string

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