Skip to Content
Errors

Errors

Stable minimums to depend on across Keel’s public runtime surfaces:

  • HTTP status code
  • machine-readable code
  • human-readable message
  • route-specific body shape

Do not assume a single universal error envelope across permit, execution, proxy, and request-level API failures.

If you need the associated permit id on execution routes, read the x-keel-permit-id response header. The execution body keeps governance focused on the decision outcome.

Error shapes by surface

SurfaceTypical result shapeParse pathNotes
POST /v1/permits deny decisionPermit object, usually not a top-level error objectdecision, reason_code, reason_detailPermit denials are decision results, not execution failures, and do not include an error object.
POST /v1/executions denied responseNormalized execution envelope with status: "denied" and errorstatus, error.code, error.message, routing.reason_codeDenied executions stay inside the normalized execution envelope.
POST /v1/executions failed responseNormalized execution envelope with status: "failed" and errorstatus, error.code, error.message, routing.reason_codeProvider or downstream execution failures still return an execution object.
POST /v1/execute denied or failed responseNormalized execution envelope with error, plus resolvedstatus, error.code, error.message, routing.reason_code, resolvedSame parsing model as /v1/executions, with one extra resolved block.
Transport, framework, or top-level API errorTop-level { "error": { ... } } objecterror.code, error.message, error.detailsUse this shape for validation, auth, not-found, idempotency-conflict, and similar request-level failures before an execution object exists.
POST /v1/proxy/* provider proxy errorProvider-native error objectProvider-native path such as error.messageProxy routes do not return the normalized execution envelope. Parse them the same way you would parse the provider directly.

For the execution routes:

  • A denied /v1/executions response is a normalized execution envelope with status: "denied".
  • A failed /v1/executions response is a normalized execution envelope with status: "failed".
  • A transport, framework, or top-level API error is a standalone { "error": ... } object because no execution envelope was created.
  • A provider proxy error is provider-native, not normalized.
  • Permit denials are decision results, not execution failures, and do not include an error object.

Examples by surface

The permit decision examples below reflect the current documented contract for permit decisions.

/v1/permits deny

HTTP status: 200

{ "id": "permit_01jyf4p6c3m7n9q2t5v8w1x4y", "decision": "deny", "reason_code": "policy.model_not_allowed", "reason_detail": { "category": "policy", "kind": "model_not_allowed", "outcome": "deny" }, "message": "The requested model is not allowed for this project.", "actions": [ { "type": "deny", "message": "The requested model is not allowed for this project." } ], "metadata": { "evaluated_at": "2026-03-26T18:43:02Z" } }

Parsing note: Permit denials are decision results, not execution failures, and do not include an error object. Read decision and reason_code; do not expect error.code.

/v1/permits allow

HTTP status: 200

{ "id": "permit_01jyf4m3n8q2r6t9v1w5x7y0z", "decision": "allow", "actions": [ { "type": "allow", "message": "Allowed by base policy." } ], "metadata": { "evaluated_at": "2026-03-26T18:42:11Z" } }

Parsing note: Allow decisions are permit results too. Parse decision; reason_code is not set on allow decisions. Do not expect error.code.

/v1/executions denied response

HTTP status: 403

{ "id": "exec_01jv1jahqf3n9s2t4u6v8w0x1z", "object": "execution", "created_at": "2026-03-09T00:00:00Z", "status": "denied", "status_code": 403, "output": null, "routing": { "requested_provider": "openai", "requested_model": "gpt-4o-mini", "selected_provider": "openai", "selected_model": "gpt-4o-mini", "reason_code": "explicit_request", "fallback_occurred": false }, "governance": { "decision": "deny", "reason": "...", "actions": [ { "type": "deny", "message": "The request did not satisfy the configured project policy." } ], "constraints": null, "budgets": null }, "error": { "code": "...", "message": "The request did not satisfy the configured project policy." } }

Parsing note: parse status, error.code, and routing.reason_code; denial is still an execution envelope.

/v1/executions execution failure

HTTP status: 502

{ "id": "exec_01jv1jahqf3n9s2t4u6v8w0x20", "object": "execution", "created_at": "2026-03-09T00:00:00Z", "status": "failed", "status_code": 502, "output": null, "routing": { "requested_provider": "openai", "requested_model": "gpt-4o-mini", "selected_provider": "openai", "selected_model": "gpt-4o-mini", "reason_code": "explicit_request", "fallback_occurred": false }, "governance": { "decision": "allow", "reason": "ok", "actions": [], "constraints": null, "budgets": null }, "error": { "code": "...", "message": "The upstream provider request failed." } }

Parsing note: treat this like a completed execution record with status: "failed" and read the failure from error.

/v1/execute denied response

HTTP status: 403

{ "id": "exec_01jv1jcm7r4p8t1u3v5w7x9y10", "object": "execution", "created_at": "2026-03-11T00:00:00Z", "status": "denied", "status_code": 403, "output": null, "routing": { "requested_provider": "openai", "requested_model": "gpt-4o-mini", "selected_provider": "openai", "selected_model": "gpt-4o-mini", "reason_code": "explicit_request", "fallback_occurred": false }, "governance": { "decision": "deny", "reason": "...", "actions": [ { "type": "deny", "message": "The request did not satisfy the configured project policy." } ], "constraints": null, "budgets": null }, "error": { "code": "...", "message": "The request did not satisfy the configured project policy." }, "resolved": { "provider": "openai", "model": "gpt-4o-mini", "alias": null } }

Parsing note: parse /v1/execute denials exactly like /v1/executions, then read resolved if you need the final target metadata.

/v1/proxy/openai provider-style error

HTTP status: 400

{ "error": { "message": "Unsupported parameter: 'max_completion_tokens'.", "type": "invalid_request_error", "param": "max_completion_tokens", "code": "unsupported_parameter" } }

Parsing note: parse this as an OpenAI-style provider error, not as a Keel execution envelope.

Generic top-level API error object

HTTP status: 409

{ "error": { "code": "idempotency_conflict", "message": "The same idempotency key was already used with a different semantic request.", "details": { "idempotency_key": "exec-sync-001" } } }

Parsing note: when the response is top-level { "error": ... }, no normalized execution object was created.

Status and retry matrix

Usual status means the documented status you should plan for in client logic, not an exhaustive guarantee for every deployment detail.

On /v1/permits, parse decision and reason_code; do not expect error.code. Allow decisions do not carry reason_code.

HTTP statusRetryable?Meaning
200Success, or a permit decision (both allow and deny return 200 on permits).
400NoThe request body, headers, or field combination is invalid.
401ConditionallyMissing or invalid credentials. Retry only after fixing auth.
403NoHard deny. The caller is not allowed to perform this action. Covers policy denials (policy.model_not_allowed, policy.rule_denied), cost cap denials (budget.daily_cap_exceeded, budget.monthly_cap_exceeded, budget.request_cap_exceeded), scope issues, and configuration problems. Does not cover throttle — see 429.
404NoThe requested resource does not exist for this project or route.
409NoThe request conflicts with an existing resource or replay contract.
429YesRate-limit throttle. Returned when a throttle_if_rate_exceeds policy rule trips (reason_code: budget.rate_limit_throttled). Includes a Retry-After header with the number of seconds to wait before retrying. Keel’s Python and JavaScript SDKs automatically parse this header and retry.
500YesKeel failed internally.
502YesThe upstream provider request failed after governance approval.

Read the error.code and error.message fields in the response body for machine-readable detail.

Permit reason codes

Every non-allow permit decision carries a reason_code from a locked vocabulary. These codes are stable API — they appear in permit responses, on the dashboard, in Timeline Replay, and in governance audit events. Filter on reason_code in your error handlers and audit queries; do not parse the human-readable message field.

CodeCategoryWhen it fires
budget.request_cap_exceededBudgetPer-request cost cap exceeded
budget.daily_cap_exceededBudgetDaily spend cap exceeded
budget.weekly_cap_exceededBudgetWeekly spend cap exceeded
budget.monthly_cap_exceededBudgetMonthly spend cap exceeded
budget.quarterly_cap_exceededBudgetQuarterly spend cap exceeded
budget.monthly_threshold_exceededBudgetProjected monthly spend exceeds configured threshold ratio
budget.daily_spike_detectedBudgetToday’s projected spend exceeds baseline × multiplier
budget.plan_quota_exceededBudgetProject has consumed its plan-tier request quota for the billing period
budget.rate_limit_exceededBudgetRequest rate exceeded — hard deny
budget.rate_limit_throttledBudgetRequest rate exceeded — throttle with Retry-After
budget.pricing_unavailableBudgetModel has no pricing configured; request cannot be safely evaluated
policy.model_not_allowedPolicyModel not in the project’s or rule’s allowed list
policy.rule_deniedPolicyA deny rule’s condition matched
policy.review_requiredPolicyA require_human_review rule’s condition matched

budget.rate_limit_throttled is the only code that produces a 429 HTTP response. All other deny codes produce 403. Throttle responses include a Retry-After header; the reason_detail.outcome_detail field carries retry_after_seconds, window_seconds, limit, and observed for programmatic use.

Firewall denial reason codes (from the Prompt Firewall subsystem, e.g. prompt_firewall_blocked) are a separate system and are not part of this lexicon.

Last updated on Edit this page on GitHub