Decisions

Optional preflight policy decision (no reservation created)

Returns ALLOW / DENY, optionally with Caps for soft landing. This endpoint does not reserve budget. Clients that require concurrency safety MUST use /v1/reservations. IDEMPOTENCY (NORMATIVE): - On replay with the same idempotency_key, the server MUST return the original successful response payload. TENANCY (NORMATIVE): - subject.tenant MUST match the effective tenant derived from auth; otherwise the server MUST return 403 FORBIDDEN. DEBT/OVERDRAFT STATE (NORMATIVE): - If the subject scope has debt > 0 or is_over_limit=true, server SHOULD return decision=DENY with reason_code=DEBT_OUTSTANDING or reason_code=OVERDRAFT_LIMIT_EXCEEDED respectively. Server MUST NOT return 409 for these conditions on /decide. CLOSED TENANT (NORMATIVE): - If the owning tenant's status is CLOSED (deployments with a governance plane), a fresh (non-replay) evaluation MUST return decision=DENY with reason_code=TENANT_CLOSED, reflecting as-if-live the 409 TENANT_CLOSED the persisting mutation surface returns (same-key replays of pre-close decisions follow the IDEMPOTENCY rule above and the replayed-decision caveat below). Server MUST NOT return 409 for this condition on /decide. A tenant record whose status cannot be determined (malformed record) MUST fail closed with 500 INTERNAL_ERROR — see the closed-tenant binding in ERROR SEMANTICS.

Idempotency on /decide is for request deduplication only. A replayed ALLOW response reflects budget state at the time of the original call; clients MUST NOT treat a replayed decision as current budget authorization.

post/v1/decide

Request

  • Base URL: https://api.cycles.local
  • URL: https://api.cycles.local/v1/decide
  • Auth: API key in header X-Cycles-API-Key

Headers

X-Idempotency-Keystring

Optional idempotency key header. If both header and body idempotency_key are provided, they MUST match. Server MUST enforce idempotency per endpoint by (effective tenant, endpoint, idempotency_key). On replay of an idempotent request that previously succeeded, server MUST return the original successful response outcome and payload (including any server-generated identifiers such as reservation_id), except that remaining_ttl_ms on a replayed createReservation or extendReservation success is a volatile response observation and is not part of the attested CyclesEvidence payload. When emitted, it MUST be recomputed from the original expires_at_ms and the current authoritative server time, never copied from the stored response; it MUST be 0 if the reservation is no longer ACTIVE. All other fields replay verbatim.

Request body

idempotency_keystring required
metadataobject

Example request

{
  "estimate": {
    "unit": "USD_MICROCENTS",
    "amount": 500000
  }
}

Response

Decision result

decision'ALLOW' | 'ALLOW_WITH_CAPS' | 'DENY' required
reason_codestring

Stable machine-readable reason for why a /decide call or a reservation returned decision=DENY. Populated on DecisionResponse.reason_code and on ReservationCreateResponse.reason_code. The human-readable message field on the parent response (when present) may carry additional context. EXTENSIBILITY (NORMATIVE): DecisionReasonCode is intentionally an OPEN string (not a closed enum) so that: (a) future minor versions of this base spec can add new values without a breaking change, and (b) companion extension specs (e.g., cycles-protocol-extensions-v0.1.26.yaml) can define additional reason codes without modifying the base schema. Clients MUST gracefully handle unknown values — log them and map to generic DENY handling (i.e., "the request was denied; treat as a terminal failure even if we don't recognize the specific reason").

KNOWN VALUES (v0.1.25 base):

  • BUDGET_EXCEEDED — remaining amount insufficient on at least one derived scope (evaluated against the requested estimate.amount).
  • BUDGET_FROZEN — a derived scope has a budget in FROZEN status (operator-set, no mutations allowed).
  • BUDGET_CLOSED — a derived scope has a budget in CLOSED status (permanently closed).
  • BUDGET_NOT_FOUND — no budget exists at any derived scope in the requested unit. On non-dry reserve and /v1/events paths this same condition surfaces as HTTP 404 with error=NOT_FOUND instead.
  • OVERDRAFT_LIMIT_EXCEEDED — either (a) debt + delta > overdraft_limit on commit, or (b) the scope is in over-limit state (is_over_limit=true) and no new reservations are permitted until reconciled.
  • DEBT_OUTSTANDING — a derived scope has debt > 0 and overdraft_limit == 0 (no policy permits further debt accrual).
  • TENANT_CLOSED — the owning tenant's status is CLOSED (deployments with a governance plane; added in revision 2026-07-10). Populated on /decide and dry_run=true evaluations; the persisting mutation surface reports the same condition as HTTP 409 error=TENANT_CLOSED instead (see the closed-tenant binding in ERROR SEMANTICS).

KNOWN VALUES (v0.1.26 runtime extension):

  • ACTION_QUOTA_EXCEEDED — a per-kind or risk-class action quota rule was exceeded for the target scope and window.
  • ACTION_KIND_DENIED — the action kind is in the matching policy's denied_action_kinds list.
  • ACTION_KIND_NOT_ALLOWED — the matching policy has a non-empty allowed_action_kinds list and the action kind is not in it. See cycles-protocol-extensions-v0.1.26.yaml for full semantics, evaluation order, and DenyDetail structure (populated alongside reason_code for v0.1.26 denials).

Distinct from the ErrorCode enum: reason codes appear only on 200 OK responses with decision=DENY; ErrorCodes appear only on 4xx/5xx responses with an error field. Some labels overlap (e.g. BUDGET_EXCEEDED appears in both sets) because the same underlying condition is reported two ways depending on the endpoint: /decide and dry_run surface it as a non-4xx DENY decision, while non-dry reserve surfaces it as a 409 error. SDK GENERATION GUIDANCE: Codegen tools SHOULD generate a string type (not a strict enum) and expose the known values above as constants or string literal union members for ergonomic use. SDK code MUST NOT reject unknown values at the deserialization boundary.

retry_after_msinteger
affected_scopesstring[]

Canonical scope identifiers impacted by this decision, in canonical order.

Example response

{
  "decision": "ALLOW",
  "affected_scopes": [
    "tenant:acme-corp",
    "tenant:acme-corp/workspace:prod"
  ]
}

Changes

No recorded changes to this operation across all 1 revision of this API.