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…"
}
}
]
}
}
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.

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 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