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. Send a ULID of your own in that header and it is used instead.
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. The table is shared by every boundary of Reelwire, so a few codes, such as replayed and timestamp_skew, belong to signed deliveries rather than to anything you call.

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, such as JSON sent to a route that takes a file.
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 outcome that sounds like a failure deliberately is not one. An event older than its feed accepts is answered 202 with status: "stale" in an ordinary response rather than in an error envelope: it was well formed and was stored, and nothing was drawn. A producer replaying history sees success rather than a wall of 4xx that would hide a real error among it. The code stale_event carries 202 in the table for the same reason. See Sending events.

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.