New Ory Agent Security is now live! Claim your complimentary test drive. Get Started!

Skip to main content

Error handling

Ory Talos API errors follow Google AIP-193, which wraps google.rpc.Status in a top-level error object.

Error response format​

Every non-2xx response uses the AIP-193 envelope:

{
"error": {
"code": 404,
"message": "API key not found",
"status": "NOT_FOUND",
"details": [
{
"@type": "type.googleapis.com/google.rpc.ErrorInfo",
"reason": "API_KEY_NOT_FOUND",
"domain": "www.ory.com/talos",
"metadata": {
"key_id": "01J9X7…"
}
},
{
"@type": "type.googleapis.com/google.rpc.RequestInfo",
"requestId": "0d7f1a4c-1b4e-4f0f-9d2f-4f0e2b6f1a3c"
}
]
}
}
FieldDescription
error.codeHTTP status code (integer). Matches the response status line.
error.messageHuman-readable summary. Suitable for logging, not for end-user display.
error.statusCanonical gRPC status name (for example NOT_FOUND, PERMISSION_DENIED).
error.detailsList of typed details. google.rpc.ErrorInfo carries the machine-readable reason and domain.

details is an open list. Select entries by @type instead of by position, and ignore types you don't handle: Talos can add detail types without a breaking change.

The HTTP status code follows the canonical gRPC-to-HTTP mapping. For example, NOT_FOUND returns HTTP 404; PERMISSION_DENIED returns HTTP 403.

error.status comes from the error's canonical gRPC status, not from the HTTP code, so two errors that share an HTTP code stay distinguishable. A state conflict returns FAILED_PRECONDITION while a duplicate resource returns ALREADY_EXISTS, even though both use HTTP 409.

Reading the reason​

The stable, machine-readable identifier is error.details[*].reason on the ErrorInfo detail. Match on reason — never on message, which can change between releases. The domain is www.ory.com/talos. For client errors, the optional metadata map carries contextual string values. Treat keys as advisory and don't require their presence.

REASON=$(echo "$RESPONSE" | jq -er '.error.details[]? | select(."@type" | endswith("ErrorInfo")).reason')

Correlating a request​

When a request carries an X-Request-ID header, Ory Talos echoes it in the response and repeats it in a google.rpc.RequestInfo detail as requestId. Ory Talos doesn't generate request IDs: without the header, the response has neither the header nor the RequestInfo detail.

REQUEST_ID=$(echo "$RESPONSE" | jq -r '.error.details[]? | select(."@type" | endswith("RequestInfo")).requestId')

Treat request IDs as opaque. They aren't guaranteed to be unique, and they carry no authorization meaning.

Ory Network​

The Ory Network edge sets X-Request-ID on every request and replaces any value you send, so you can't correlate with your own ID. When you contact Ory support, quote the cf-ray response header, which Cloudflare adds to every response, or the requestId.

Self-hosted​

Set X-Request-ID at your proxy or load balancer, or send it from the client. Ory Talos drops an ID longer than 128 bytes instead of shortening it. Search your Ory Talos logs for the ID to find the matching entry. A request that arrives without X-Request-ID has no request ID in Ory Talos logs, spans, or error responses, so set one at the proxy to correlate a client error with server logs.

Browser clients need CORS to send and read the header. When serve.http.cors.enabled is true, the default serve.http.cors.allowed_headers and serve.http.cors.exposed_headers already cover X-Request-ID. If you set either list yourself, it replaces the default, so include X-Request-ID in both: the allowed list lets the browser send the ID, and the exposed list lets it read the echoed ID.

Server errors are opaque​

5xx responses never describe what failed inside the server. error.message is always the generic message An internal server error occurred, please contact the system administrator. The metadata map is omitted, and reason is one of INTERNAL_SERVER_ERROR, SERVICE_UNAVAILABLE, or GATEWAY_TIMEOUT:

{
"error": {
"code": 503,
"message": "An internal server error occurred, please contact the system administrator",
"status": "UNAVAILABLE",
"details": [
{
"@type": "type.googleapis.com/google.rpc.ErrorInfo",
"reason": "SERVICE_UNAVAILABLE",
"domain": "www.ory.com/talos"
},
{
"@type": "type.googleapis.com/google.rpc.RequestInfo",
"requestId": "0d7f1a4c-1b4e-4f0f-9d2f-4f0e2b6f1a3c"
}
]
}
}

error.code and error.status still identify the condition, so keep using them to decide whether to retry. The underlying error stays in the server logs.

Verification errors​

The verify endpoint (POST /v2alpha1/admin/apiKeys:verify) is an exception. A verification failure is part of the normal verification result, not a transport-level error, so it returns 200 OK with is_valid: false and a structured error code:

{
"is_valid": false,
"error_code": "VERIFICATION_ERROR_REVOKED",
"error_message": "The API key has been revoked."
}

Treat the response as successful; act on is_valid and error_code. Only fall back to the AIP-193 envelope above when the HTTP status isn't 2xx (for example, when the verify request itself is malformed).

For the complete list of verification error codes (VERIFICATION_ERROR_*), see the error codes reference.

HTTP status codes​

For the complete list of HTTP status codes and reasons, see the error codes reference.

Key categories:

  • 4xx errors: Client errors (bad request, not found, conflict). Fix the request before retrying.
  • 5xx errors: Server errors. Retry with exponential backoff.

Retry strategy​

Safe to retry​

  • UNAVAILABLE (HTTP 503) — the server is temporarily overloaded. Retry with exponential backoff. Honor the Retry-After header when present.
  • DEADLINE_EXCEEDED (HTTP 504) — the request timed out. Retry with backoff.
  • RESOURCE_EXHAUSTED (HTTP 429) — the server throttled the request. Honor the Retry-After header when present, then retry with backoff.
  • Network errors — connection refused, DNS failure, and so on. Retry with backoff.

Not safe to retry without an idempotency key​

  • ALREADY_EXISTS (HTTP 409) — the resource already exists. Read the existing resource and reconcile.
  • FAILED_PRECONDITION (HTTP 409) — the operation didn't meet a precondition. It shares HTTP 409 with ALREADY_EXISTS, but error.status distinguishes the two. Fix the precondition before retrying.
  • INVALID_ARGUMENT (HTTP 400) — fix the request before retrying.

Idempotency key​

When issuing API keys, include request_id in the request body to deduplicate retries:

# request_id is only available via the HTTP API.
talos keys issue "my-service" --actor user_1 -e "$TALOS_URL"

Repeating the same request_id returns the originally created key instead of issuing a new one (AIP-155). Use a stable, client-generated UUID per logical issuance. The value is capped at 36 characters.

attempt 1: wait 100ms
attempt 2: wait 200ms
attempt 3: wait 400ms
attempt 4: wait 800ms
attempt 5: wait 1600ms (give up after this)

Add jitter (random 0-50% of the wait time) to avoid thundering-herd effects.

Next steps​