Error Codes and Error Handling in Cycles
Cycles uses structured error responses with specific error codes for every failure condition.
Understanding these codes is essential for building a production integration. Each code tells the client exactly what happened and what to do about it.
Error response format
Every error response follows the same structure:
{
"error": "BUDGET_EXCEEDED",
"message": "Insufficient budget in scope tenant:acme",
"request_id": "req-abc-123",
"details": {}
}- error — a machine-readable error code from the fixed enum
- message — a human-readable explanation
- request_id — a unique identifier for debugging and support
- details — optional additional context
The error codes
Cycles defines 12 error codes, each with a specific HTTP status code and meaning.
INVALID_REQUEST (400)
The request is malformed or missing required fields.
Common causes:
- missing required fields (subject, action, estimate, idempotency_key)
- Subject with only
dimensionsand no standard field (tenant, workspace, app, workflow, agent, toolset) - field values exceeding length limits
- invalid parameter values
What to do: fix the request. This is not retryable without changes.
UNAUTHORIZED (401)
The X-Cycles-API-Key header is missing or the API key is invalid.
What to do: check the API key configuration. Not retryable without a valid key.
FORBIDDEN (403)
The request is authenticated but not authorized for the target resource.
Common causes:
- Subject.tenant does not match the effective tenant derived from the API key
- attempting to commit/release/extend a reservation owned by a different tenant
- querying balances for a different tenant
What to do: ensure the tenant in the Subject matches the API key's tenant. Not retryable without fixing the tenant mismatch.
NOT_FOUND (404)
The specified reservation does not exist.
This is different from RESERVATION_EXPIRED — a 404 means the reservation was never created, while RESERVATION_EXPIRED means it existed but its TTL has passed.
What to do: verify the reservation ID. If the client lost the ID, use GET /v1/reservations with the idempotency_key filter to recover it.
BUDGET_EXCEEDED (409)
Budget is insufficient for the requested operation.
This appears in four contexts:
- Reservation: the scope does not have enough remaining budget for the estimate
- Commit with REJECT policy: actual exceeds reserved
- Commit with ALLOW_IF_AVAILABLE policy: remaining budget cannot cover the delta
- Event with REJECT or ALLOW_IF_AVAILABLE: insufficient budget for the event amount
What to do: depends on context:
- for reservations: degrade (smaller model, fewer tools), defer, or deny the action
- for commits: the work already happened — consider switching to ALLOW_IF_AVAILABLE or ALLOW_WITH_OVERDRAFT
- for events: adjust the amount or change the overage policy
RESERVATION_EXPIRED (410)
The reservation's TTL plus grace period has elapsed.
The reservation has been finalized as EXPIRED and its budget has been returned to the pool.
What to do: create a new reservation if the work still needs to proceed. If the work already completed, the usage may need to be recorded as an event instead.
RESERVATION_FINALIZED (409)
An operation was attempted on a reservation that is already in a terminal state (COMMITTED or RELEASED).
This typically happens when trying to extend a reservation that has already been committed.
What to do: no action needed on the reservation. If the extend was meant to keep a different reservation alive, check the reservation ID.
IDEMPOTENCY_MISMATCH (409)
The same idempotency key was used with a different request payload.
This means the client sent a request with an idempotency key that was already used for a different operation.
What to do: use a unique idempotency key for each distinct operation. If this is a legitimate retry, ensure the request payload matches the original exactly.
UNIT_MISMATCH (400)
The unit in the request does not match the expected unit.
Common causes:
- committing with a different unit than the reservation was created with (e.g., reserving in TOKENS, committing in USD_MICROCENTS)
- creating an event with a unit not supported for the target scope
What to do: ensure the unit is consistent across the reservation lifecycle. Check that the commit or event uses the same unit as the reservation or scope.
OVERDRAFT_LIMIT_EXCEEDED (409)
Appears in two contexts:
- During commit: when
overage_policy=ALLOW_WITH_OVERDRAFTand(current_debt + delta) > overdraft_limit - During reservation: when the scope is in over-limit state (
is_over_limit=true) due to prior concurrent commits pushing debt past the limit
What to do:
- if during commit: the debt limit has been reached. The work already happened. An operator needs to fund the scope.
- if during reservation: the scope is blocked. Wait for debt to be repaid, or escalate to an operator. The client should retry with exponential backoff.
DEBT_OUTSTANDING (409)
A new reservation was attempted against a scope that has outstanding debt (debt > 0).
Even if the scope has not exceeded its overdraft limit, any debt blocks new reservations until it is repaid.
What to do: wait for debt to be repaid through budget funding. Retry with exponential backoff, or escalate to an operator.
Note: when is_over_limit=true, the server returns OVERDRAFT_LIMIT_EXCEEDED instead of DEBT_OUTSTANDING, even if debt > 0. OVERDRAFT_LIMIT_EXCEEDED takes precedence.
INTERNAL_ERROR (500)
An unexpected server error occurred.
What to do: retry with exponential backoff. If the error persists, contact the Cycles server operator.
Error handling by operation
Reserve errors
| Error | HTTP | Meaning |
|---|---|---|
| BUDGET_EXCEEDED | 409 | Insufficient budget |
| OVERDRAFT_LIMIT_EXCEEDED | 409 | Scope is over-limit |
| DEBT_OUTSTANDING | 409 | Scope has unresolved debt |
| IDEMPOTENCY_MISMATCH | 409 | Same key, different payload |
| INVALID_REQUEST | 400 | Malformed request |
| UNAUTHORIZED | 401 | Invalid API key |
| FORBIDDEN | 403 | Tenant mismatch |
Decide errors
| Error | HTTP | Meaning |
|---|---|---|
| INVALID_REQUEST | 400 | Malformed request |
| UNAUTHORIZED | 401 | Invalid API key |
| FORBIDDEN | 403 | Tenant mismatch |
| IDEMPOTENCY_MISMATCH | 409 | Same key, different payload |
Note: decide returns 200 with decision: DENY for budget or debt conditions, not a 409 error.
Commit errors
| Error | HTTP | Meaning |
|---|---|---|
| BUDGET_EXCEEDED | 409 | Actual exceeds budget (REJECT or ALLOW_IF_AVAILABLE) |
| OVERDRAFT_LIMIT_EXCEEDED | 409 | Debt would exceed limit (ALLOW_WITH_OVERDRAFT) |
| RESERVATION_EXPIRED | 410 | Past TTL + grace period |
| RESERVATION_FINALIZED | 409 | Already committed or released |
| UNIT_MISMATCH | 400 | Unit differs from reservation |
| NOT_FOUND | 404 | Reservation never existed |
| IDEMPOTENCY_MISMATCH | 409 | Same key, different payload |
| UNAUTHORIZED | 401 | Invalid API key |
| FORBIDDEN | 403 | Reservation owned by different tenant |
Release errors
| Error | HTTP | Meaning |
|---|---|---|
| RESERVATION_EXPIRED | 410 | Past TTL + grace period |
| RESERVATION_FINALIZED | 409 | Already committed or released |
| IDEMPOTENCY_MISMATCH | 409 | Same key, different payload |
| NOT_FOUND | 404 | Reservation never existed |
| UNAUTHORIZED | 401 | Invalid API key |
| FORBIDDEN | 403 | Reservation owned by different tenant |
Extend errors
| Error | HTTP | Meaning |
|---|---|---|
| INVALID_REQUEST | 400 | Missing or invalid fields |
| RESERVATION_EXPIRED | 410 | Past TTL (no grace period for extend) |
| RESERVATION_FINALIZED | 409 | Already committed or released |
| IDEMPOTENCY_MISMATCH | 409 | Same key, different payload |
| NOT_FOUND | 404 | Reservation never existed |
| UNAUTHORIZED | 401 | Invalid API key |
| FORBIDDEN | 403 | Reservation owned by different tenant |
Event errors
| Error | HTTP | Meaning |
|---|---|---|
| BUDGET_EXCEEDED | 409 | Insufficient budget (REJECT or ALLOW_IF_AVAILABLE) |
| OVERDRAFT_LIMIT_EXCEEDED | 409 | Debt would exceed limit (ALLOW_WITH_OVERDRAFT) |
| UNIT_MISMATCH | 400 | Unit not supported for scope |
| INVALID_REQUEST | 400 | Malformed request |
| UNAUTHORIZED | 401 | Invalid API key |
| FORBIDDEN | 403 | Tenant mismatch |
| IDEMPOTENCY_MISMATCH | 409 | Same key, different payload |
Idempotency and error handling
Errors interact with idempotency in specific ways:
- Successful replay: if you retry a request with the same idempotency key and payload, you get the original successful response. The operation is not applied again.
- Payload mismatch: if you reuse a key with a different payload, you get
409 IDEMPOTENCY_MISMATCH. - Failed original: if the original request failed (e.g., BUDGET_EXCEEDED), retrying with the same key sends a fresh request. Idempotency only applies to successful operations.
The request_id field
Every error response includes a request_id.
This is a server-generated identifier useful for:
- correlating errors with server logs
- debugging with the Cycles server operator
- tracking specific failures in client-side monitoring
Log the request_id when handling errors.
Summary
Cycles provides 12 specific error codes that tell the client exactly what went wrong:
- 400 for request validation issues (INVALID_REQUEST, UNIT_MISMATCH)
- 401 for authentication failures (UNAUTHORIZED)
- 403 for authorization failures (FORBIDDEN)
- 404 for missing reservations (NOT_FOUND)
- 409 for budget and state conflicts (BUDGET_EXCEEDED, OVERDRAFT_LIMIT_EXCEEDED, DEBT_OUTSTANDING, RESERVATION_FINALIZED, IDEMPOTENCY_MISMATCH)
- 410 for expired reservations (RESERVATION_EXPIRED)
- 500 for server errors (INTERNAL_ERROR)
Handling these codes correctly is the difference between a fragile integration and a production-grade one.
Next steps
To explore the Cycles stack:
- Read the Cycles Protocol
- Run the Cycles Server
- Manage budgets with Cycles Admin
- Integrate with Python using the Python Client
- Integrate with TypeScript using the TypeScript Client
- Integrate with Spring AI using the Spring Client
