Skip to main content

Errors

Every failure comes back in one envelope, whichever route refused it:

{
"error": {
"type": "https://reelwire.io/errors/forbidden",
"code": "forbidden",
"message": "This key may not read here. Give it the \"Team: read\" permission, or use a key that has it.",
"requestId": "01M2SR17R25EKN6DM1H9DVDS0D",
"details": []
}
}

The fields, and what each is for:

FieldWhat it is
codeThe machine-readable reason. Branch on this, never on the message.
typeA stable documentation URI, https://reelwire.io/errors/{code}. It carries no extra information beyond the code.
messageWritten for a person to read and safe to show in your own interface. It never carries a stack trace or a secret.
requestIdA ULID that correlates this failure with the log line in every container the request touched. The same value is on the x-request-id response header of every response, successful or not.
detailsOne entry per problem, each with a JSON Pointer into the request body.
retryAfterSecondsPresent on rate_limited and upstream_unavailable.

Quote the requestId when you ask about a failure. It is the one value that finds the request again.

Details carry a pointer per problem

details is what makes a refusal actionable: a client can point at the exact field rather than print a paragraph and leave somebody to find it.

curl http://localhost:4000/v1/stream \
-H "Authorization: Bearer rw_your_key_here" \
-H "Content-Type: application/json" \
-d '{"streamId":"st_30472168d941","eventId":"not-a-ulid","occurredAt":"yesterday","data":{}}'
{
"error": {
"type": "https://reelwire.io/errors/schema_validation",
"code": "schema_validation",
"message": "Stream envelope is not valid",
"requestId": "01M2SQNH1MDKPWQ83Y3ECG60M3",
"details": [
{ "path": "/eventId", "message": "must be a Crockford base32 ULID", "rule": "invalid_string" },
{ "path": "/occurredAt", "message": "Invalid datetime", "rule": "invalid_string" }
]
}
}

path is an RFC 6901 JSON Pointer into the body you sent. An empty string means the document as a whole. rule is an optional machine hint, and expected and received appear where the checker knew both.

The codes, and the status each carries

A route never picks its own status: the code decides it, in one table.

CodeStatusWhen
malformed_json400The body is not JSON.
schema_validation400Well-formed JSON that does not fit the shape.
ratio_not_supported400The template does not come in that shape.
locale_not_supported400Nothing is drawn in that language.
blocked_url400A URL that Reelwire will not fetch or deliver to.
unauthenticated401Missing or invalid credentials.
signature_invalid401A signature that does not check out.
timestamp_skew401A signed request too far from now.
forbidden403A valid credential that may not do this.
not_found404No such thing, or not yours.
unknown_source404No shipped feed under that source slug.
unknown_format404No such format under that source.
unknown_template404No such template.
unknown_stream404No stream with that id on this account.
conflict409A conflict with what is already there.
idempotency_conflict409The same eventId with a different body.
invalid_state_transition409The thing is not in a state this action applies to.
stream_disabled409The stream exists and is switched off.
no_sample409Asked about a stream's own field names before it has received anything.
template_not_approved409The template is not cleared for this workspace.
replayed409A delivery id that has already been processed.
payload_too_large413Over the size limit for this route.
unsupported_media_type415The wrong Content-Type.
rate_limited429Too many requests. retryAfterSeconds says how long to wait.
internal500A fault on Reelwire's side.
render_failed502The renderer refused the job.
asset_unavailable502A file the render needed could not be fetched.
checksum_mismatch502A file arrived but is not the file that was promised.
upstream_unavailable503A dependency is down, such as every renderer being unreachable.
render_timeout504The renderer took too long.

One code is deliberately not a failure. stale_event is served with 202: the event was well formed and was stored, it was older than the feed accepts, so nothing was drawn. A producer replaying history sees success rather than a wall of 4xx that would hide a real error among it.

404 and 403 are not interchangeable

A thing that belongs to another workspace answers exactly as a thing that does not exist. That is on purpose: telling a stranger that st_000000000000 exists but is not theirs is telling them something. A 403 means the credential is yours and is not allowed the action, and the message names the permission to add. See Permissions.

Retrying

429 and 503 are worth retrying, after retryAfterSeconds where it is given and with a backoff otherwise. 502 and 504 on a render are worth one retry. Everything else in the 4xx range is not: the request will be refused the same way until it changes.

Sends carry an eventId that makes a retry idempotent, so retrying a timeout cannot post twice. See Sending events.