Skip to main content
Kordio Control has two kinds of failure and they are not the same thing. A refusal is a policy answer with a full decision in the body. An error is a request that could not be evaluated at all.

A denial is not an error

A denied decision answers 403 and carries the whole decision, not just a code:
403 Forbidden
headroom is measured before the reservation, so on an allowed decision it still counts the amount you were just authorized to spend. policy_snapshot is the policy as it read at that instant, which is what lets a decision be re-read months later against a rule that has since changed.
A 403 from the agent API is a policy answer, not a transport failure. Do not retry it with a fresh idempotency key: that asks the same question again and, for a held action, asks a person twice. Change the action or change the policy.

Rules that can refuse

decision.rule names what decided. Rules created through the typed policy columns report under the column’s name, not the underlying kind. See the mapping table. A rule written with a custom rule_name reports under that name instead. An approval_threshold never appears as a denial. It does not refuse; it answers 202 and holds the action. See holding spend for a person.

What detail carries

decision.detail is keyed by the rule that decided, not by a shared schema. Read it by rule: The amount ceilings report limit_cents, not max_cents, and the approval threshold reports threshold_cents, not above_cents. Those are the names you wrote the rule with, not the names it answers with. An allowed decision has rule: null and detail: {}. Nothing decided it; nothing refused.

Error envelope

Everything that is not a decision returns:
Switch on code where it is present. message is for humans and may change. code is null for most validation failures; only the codes below ever set it. Branch on the HTTP status first and treat code as a refinement, not as the primary signal.

Codes

runtime_key_required

401 - No agent key was presented. Send Authorization: Bearer krt_....

invalid_runtime_key

401 - The key was not recognised, or its agent is suspended or revoked.

identity_token_required

401 - No dashboard token was presented on a /control/v1/workspaces/* or /v1/account call. Agent keys do not reach these routes; send a WorkOS access token.

invalid_identity_token

401 - The dashboard token was rejected: malformed, expired, or signed by something this environment does not trust.

budget_ceiling

422 - The budget asked for is larger than the agent’s max_budget_cents, which is set from the dashboard and cannot be raised with an agent key. The body carries ceiling_cents and requested_cents so the caller can open a smaller one rather than guess.

parent_budget_exhausted

422 - A child budget was asked for that is larger than the parent has left. Children reserve from their parent when they are opened, so several children cannot sum past it. ceiling_cents is what the parent actually has left.

insufficient_role

403 - Your membership does not carry the capability this call requires. Approving needs approve; writing policy and managing agents need manage_policy; members and billing need manage_members.

idempotency_conflict

409 - A request with this Idempotency-Key is still in flight, so there is no stored response to replay yet. Wait and retry with the same key; do not mint a new one.

idempotency_key_required

422 - POST /control/v1/agent/actions requires an Idempotency-Key header. Note the status: this is a 422, not a 400.
POST /control/v1/agent/payment_intents does not use that header. It requires an idempotency_key field in the body, and its absence is a 400. The two authorize endpoints are asymmetric here.

authorization_required

400 - POST /control/v1/cosign/verify was called with no token, in neither the authorization body field nor an Authorization header.

rate_limited

429 - Back off per the Retry-After header. Agent endpoints allow 1200 requests per minute per key; workspace endpoints allow 300.

billing_unavailable

503 - Billing could not be reached, so a call that had to check entitlement could not be answered. Authorization is unaffected; retry.

identity_unavailable

503 - The identity provider could not be reached during sign-in.

identity_not_configured

503 - This deployment has no identity provider configured.

mfa_required

401 - Sign-in needs a second factor. The response carries pending_authentication_token and authentication_challenge_id alongside error; complete at POST /v1/auth/verify_mfa.

email_verification_required

401 - The account exists but its email is unverified. The response carries pending_authentication_token; complete at POST /v1/auth/verify_email.

invalid_signature

401 - An inbound billing webhook failed HMAC verification.

Statuses without a code

Next steps

How a decision is reached

Why the first denial wins, and what fails closed.

Writing a policy

The rules behind every name in the table above.