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"
}
]
}
}
| Field | Description |
|---|---|
error.code | HTTP status code (integer). Matches the response status line. |
error.message | Human-readable summary. Suitable for logging, not for end-user display. |
error.status | Canonical gRPC status name (for example NOT_FOUND, PERMISSION_DENIED). |
error.details | List 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 theRetry-Afterheader when present.DEADLINE_EXCEEDED(HTTP 504) — the request timed out. Retry with backoff.RESOURCE_EXHAUSTED(HTTP 429) — the server throttled the request. Honor theRetry-Afterheader 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 HTTP409withALREADY_EXISTS, buterror.statusdistinguishes 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:
- CLI
- curl
# request_id is only available via the HTTP API.
talos keys issue "my-service" --actor user_1 -e "$TALOS_URL"
curl -s -X POST "$TALOS_URL/v2alpha1/admin/issuedApiKeys" \
-H "Content-Type: application/json" \
-d '{
"name": "my-service",
"actor_id": "user_1",
"request_id": "unique-request-id-123"
}' | jq .
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.
Recommended backoff
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
- Issue and verify — verification response format
- Batch operations — partial failure handling
