Skip to navigation

Errors

RFC 9457 problem details with stable machine-readable codes

Every error — from any endpoint, any status — is a flat RFC 9457 body with content type application/problem+json:

{
"type": "https://api.rray.app/errors/message.not_found",
"title": "Message not found",
"status": 404,
"code": "message.not_found",
"category": "not_found",
"detail": "The message does not exist or is not visible to your credential.",
"request_id": "df7b848fabd945889820b257e004c894",
"instance": "/v1/messages/msg_00000000000000000000000000"
}
FieldWhat it is
codeThe stable machine key. Match on this.
categoryCoarse “how should I react” bucket — see below.
typeDereferenceable URL for code in the public catalog.
titleShort human label. For display, not for matching.
detailConstant, client-safe sentence explaining what to do.
statusThe HTTP status, repeated in the body.
request_idQuote this to support; also returned as RRay-Request-Id.
instanceThe request path that produced the error.

Match on code, not on text

code is the contract: dotted, namespaced (<domain>.<outcome>), and never reused for a different meaning. title is for humans and may be reworded; status alone is too coarse (several unrelated conflicts all map to 409).

if problem["code"] == "rate_limit.exceeded":
sleep(int(response.headers["Retry-After"]))
elif problem["code"].startswith("idempotency."):
...

Switch on category for retry policy

When you want one branch per kind of failure rather than per code, use category. It is deliberately small and stable:

CategoryReact by
validationFixing the request — it will never succeed as sent
authenticationPresenting a valid credential
authorizationUsing a credential that is allowed to do this
not_foundNot retrying; the resource does not exist for you
conflictResolving the state clash (idempotency, uniqueness)
rate_limitWaiting out Retry-After, then retrying
billingRaising a limit, paying, or upgrading the plan
upstreamRetrying — a downstream dependency failed
serverRetrying; if it persists, contact support

Every type URL resolves

Follow the type link, or browse GET /errors for the whole catalog and GET /errors/{code} for one entry. The catalog is generated from the same registry the API raises from, so it is never out of date. It is unauthenticated — it is the documentation. Each API Reference endpoint also documents exactly the errors it can return, with examples.

Structured causes

Errors that point at a specific cause carry one extra array.

invalid_params locates a bad request field or header:

{
"code": "validation.request_invalid",
"category": "validation",
"status": 422,
"invalid_params": [
{"field": "to", "message": "field required", "kind": "missing"}
]
}

422 validation.request_invalid is the schema-validation failure and always carries invalid_params, one entry per failing field. Errors about the state of a resource rather than a field carry state_errors in the same shape, with resource and resource_id in place of field.

For POST /v1/messages/batch in permissive mode, each failing position carries this same problem shape inside its own items[].error object, so a per-item failure reads exactly like a top-level one.

When you contact support

Quote the request_id — it links your response to our internal logs exactly.