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…"
}
}
]
}
}
| 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. |
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. The metadata map is optional and carries
arbitrary contextual key-value strings set by the server. Treat any keys as advisory and don't require their presence.
REASON=$(echo "$RESPONSE" | jq -er '.error.details[]? | select(."@type" | endswith("ErrorInfo")).reason')
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
