Reservations

Reserve budget for a planned action (concurrency-safe)

Atomically reserves the estimated amount across server-derived scopes and returns a reservation_id. Reservations expire at expires_at_ms; commits are accepted through (expires_at_ms + grace_period_ms). If dry_run=true, server MUST evaluate the full reservation request and return decision/caps/affected_scopes/balances as if the reservation were live, but MUST NOT modify balances, persist a reservation, or require commit/release. DRY-RUN RESPONSE RULES (NORMATIVE): - reservation_id and expires_at_ms MUST be absent. - affected_scopes MUST be populated regardless of decision outcome (ALLOW / ALLOW_WITH_CAPS / DENY). - If decision=ALLOW_WITH_CAPS, caps MUST be present; otherwise caps MUST be absent. - If decision=DENY, reason_code SHOULD be populated; it is the primary diagnostic signal for why the dry_run was denied. - If the owning tenant's status is CLOSED (deployments with a governance plane), dry_run MUST NOT return 409 TENANT_CLOSED; a fresh (non-replay) evaluation MUST return decision=DENY with reason_code=TENANT_CLOSED, reflecting as-if-live the 409 the persisting create returns (same-key replays of pre-close evaluations follow the IDEMPOTENCY rule below). A tenant record whose status cannot be determined (malformed record) MUST fail closed with 500 INTERNAL_ERROR even on dry_run — see the closed-tenant binding in ERROR SEMANTICS.

  • balances MAY be populated (recommended for operator visibility), but MUST reflect a non-mutating evaluation. OVER-LIMIT BLOCKING (NORMATIVE): - If ANY affected scope has debt > overdraft_limit (is_over_limit=true), the reservation MUST be rejected with 409 OVERDRAFT_LIMIT_EXCEEDED, regardless of available remaining budget.
  • This blocks new work when overdraft reconciliation is needed. IDEMPOTENCY (NORMATIVE): - On replay with the same idempotency_key, the server MUST return the original successful response outcome and payload, including the original reservation_id (if any), except that remaining_ttl_ms is a volatile transport observation not included in the attested CyclesEvidence payload. When emitted, it MUST be recomputed from the original expires_at_ms and current authoritative server time while constructing the replay response, MUST NOT be copied from the stored response, and MUST be 0 if the reservation is no longer ACTIVE. All other fields replay verbatim.

TENANCY (NORMATIVE): - subject.tenant MUST match the effective tenant derived from auth; otherwise the server MUST return 403 FORBIDDEN.

post/v1/reservations

Request

  • Base URL: https://api.cycles.local
  • URL: https://api.cycles.local/v1/reservations
  • 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
ttl_msinteger
grace_period_msinteger

Grace window after TTL for in-flight commits. Default 5s. OPERATIONAL GUIDANCE: - For high-latency actions (streaming LLM, slow APIs), consider setting higher (10-30s). INTEROP / SAFETY RATIONALE: - The 60s maximum is an interoperability ceiling to limit long-lived "in-flight" commit windows and reduce zombie-reservation and abuse surface (grace extends the time that a crashed client can keep budget locked).

LONG-RUNNING WORKFLOWS: - For multi-minute operations, clients SHOULD keep ttl_ms relatively short and use /v1/reservations/{reservation_id}/extend as a heartbeat, and/or use chunked reserve/commit cycles rather than relying on large grace_period_ms.

overage_policy'REJECT' | 'ALLOW_IF_AVAILABLE' | 'ALLOW_WITH_OVERDRAFT'

How server handles commits where actual > reserved. REJECT: reject commit (client must reserve a buffer or over-estimate). Recommended: add 10-20% buffer to estimates when using REJECT. If the action already happened externally, this creates an unaccounted gap.

ALLOW_IF_AVAILABLE (default): commit always succeeds when actual > reserved. If remaining budget supports the full delta, charge it atomically across all derived scopes. Otherwise, cap the delta to available remaining (minimum across all affected scopes, floor 0), charge estimate + capped_delta, and set is_over_limit=true on scopes where the full delta could not be covered. This blocks future reservations until reconciled. Never creates debt. Never rejects a commit — the action already happened. ALLOW_WITH_OVERDRAFT: if remaining budget supports the delta, commit normally. Otherwise, check if (debt + delta) <= overdraft_limit across all affected scopes. If yes: commit succeeds, add delta to debt, remaining can go negative. If no: commit fails with 409 OVERDRAFT_LIMIT_EXCEEDED. This policy ensures the ledger always reflects reality even when budget is exhausted at commit time. CONCURRENCY (NORMATIVE):

  • The overdraft_limit check is performed per-commit and is NOT atomic across concurrent commits to the same scope.
  • Multiple commits may each individually pass the (debt + delta) <= limit check but collectively cause debt to exceed overdraft_limit.
  • This is acceptable: all commits succeed (actions already happened), and the scope enters "over-limit" state, blocking future reservations until reconciled.
  • Servers SHOULD use optimistic locking or compare-and-swap on debt updates to ensure individual commit atomicity, but cross-commit atomicity is not required.
dry_runboolean

Shadow-mode evaluation. If true, the server MUST evaluate the reservation request and return decision/caps/affected_scopes as if it would reserve, but MUST NOT modify balances, persist a reservation, or require commit/release. Intended for safe rollout and testing of full reserve-path logic.

metadataobject

Example request

{
  "idempotency_key": "idem_20260412_run42_step1",
  "subject": {
    "tenant": "acme-corp",
    "workspace": "prod",
    "agent": "summarizer"
  },
  "action": {
    "kind": "llm.completion",
    "name": "summarize-document"
  },
  "estimate": {
    "unit": "USD_MICROCENTS",
    "amount": 500000
  },
  "ttl_ms": 30000
}

Response

Reservation decision (ALLOW/DENY with optional caps)

decision'ALLOW' | 'ALLOW_WITH_CAPS' | 'DENY' required
reservation_idstring

Present if decision is ALLOW or ALLOW_WITH_CAPS and dry_run is false. MUST be absent when dry_run is true.

expires_at_msinteger
remaining_ttl_msinteger

Observation of remaining reservation lifetime in milliseconds while this response payload is constructed, computed as max(0, expires_at_ms - current authoritative server time) using the same authoritative clock used for reservation expiry decisions. Servers SHOULD emit it on successful responses that return a live reservation; it MUST be absent on dry-run and DENY responses. This field is volatile transport metadata and is not part of the attested CyclesEvidence payload. On a same-key idempotent replay, a server that emits it MUST recompute it at replay-response construction time from the original expires_at_ms and MUST NOT copy its originally stored value; it MUST be 0 if the reservation is no longer ACTIVE and MAY conservatively understate current lead if a later, separately keyed extension moved expiry outward. All other fields replay verbatim. Added in 0.1.25.16 (additive); clients MUST treat it as optional for backward compatibility. When present it is the NORMATIVE input to heartbeat scheduling — see HEARTBEAT GUIDANCE on extendReservation.

scope_pathstring

Canonical scope path (server-derived)

affected_scopesstring[] required

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

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

Example response

{
  "decision": "ALLOW",
  "reservation_id": "rsv_a1b2c3d4",
  "reserved": {
    "unit": "USD_MICROCENTS",
    "amount": 500000
  },
  "expires_at_ms": 1712956830000,
  "remaining_ttl_ms": 600000,
  "scope_path": "tenant:acme-corp/workspace:prod/agent:summarizer",
  "affected_scopes": [
    "tenant:acme-corp",
    "tenant:acme-corp/workspace:prod",
    "tenant:acme-corp/workspace:prod/agent:summarizer"
  ]
}

Changes

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