---
title: "Optional preflight policy decision (no reservation created)"
method: POST
path: "/v1/decide"
tags: ["Decisions"]
---

# Optional preflight policy decision (no reservation created)

`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`

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.

## Headers

- `X-Idempotency-Key` string

## Request body

- object
  - `idempotency_key` string, required
  - `subject` union, required: Dimension bag for hierarchical budgets. At least one standard field (tenant, workspace, app, workflow, agent, or toolset) MUST be provided. A subject containing only `dimensions` is invalid; server MUST return 400 INVALID_REQUEST. Hierarchy: tenant → workspace → app → workflow → agent → toolset. CHARSET (NORMATIVE, revision 2026-07-03): - Standard-field values (tenant/workspace/app/workflow/agent/toolset) SHOULD match ^[a-zA-Z0-9_.-]+$. Canonical scope identifiers and scope paths use ":" and "/" as structural delimiters, so delimiter, whitespace, and control characters have no stable canonical encoding. - Servers MAY reject standard-field values outside this pattern with 400 INVALID_REQUEST (the reference implementation does). Clients MUST NOT rely on out-of-pattern values being accepted, and portable clients SHOULD restrict themselves to the pattern above. EXTENSIBILITY (v0): - dimensions is an optional, user-defined map for alternative taxonomies (e.g., cost_center/department/project) and policy/reporting. - v0 servers MAY ignore dimensions for budgeting decisions; if they do, they still MUST accept and round-trip it. - Keys SHOULD be lowercase and match ^[a-z0-9_.-]+$ for stable canonicalization; values are opaque strings.
    - object
      - `tenant` string, required
      - `workspace` string
      - `app` string
      - `workflow` string
      - `agent` string
      - `toolset` string
      - `dimensions` object: Optional custom dimensions for enterprise taxonomies and policy/reporting.
    - object
      - `tenant` string
      - `workspace` string, required
      - `app` string
      - `workflow` string
      - `agent` string
      - `toolset` string
      - `dimensions` object: Optional custom dimensions for enterprise taxonomies and policy/reporting.
    - object
      - `tenant` string
      - `workspace` string
      - `app` string, required
      - `workflow` string
      - `agent` string
      - `toolset` string
      - `dimensions` object: Optional custom dimensions for enterprise taxonomies and policy/reporting.
    - object
      - `tenant` string
      - `workspace` string
      - `app` string
      - `workflow` string, required
      - `agent` string
      - `toolset` string
      - `dimensions` object: Optional custom dimensions for enterprise taxonomies and policy/reporting.
    - object
      - `tenant` string
      - `workspace` string
      - `app` string
      - `workflow` string
      - `agent` string, required
      - `toolset` string
      - `dimensions` object: Optional custom dimensions for enterprise taxonomies and policy/reporting.
    - object
      - `tenant` string
      - `workspace` string
      - `app` string
      - `workflow` string
      - `agent` string
      - `toolset` string, required
      - `dimensions` object: Optional custom dimensions for enterprise taxonomies and policy/reporting.
  - `action` object, required
    - `kind` string, required: Action type identifier. Recommended format: <category>.<operation> Examples: llm.completion, llm.embedding, tool.search, tool.calculator, db.query, db.write, http.get, http.post, file.upload, message.email, webhook.outbound
    - `name` string, required: Provider/model/tool identifier (e.g., "openai:gpt-4o-mini", "web.search")
    - `tags` string[]: Optional policy tags (e.g., ["prod","customer-facing"])
  - `estimate` object, required
    - `unit` 'USD_MICROCENTS' | 'TOKENS' | 'CREDITS' | 'RISK_POINTS', required: Standard units. USD_MICROCENTS preserves precision for per-call and batched accounting (int64). - 1 USD_MICROCENTS = 10^-6 cents = 10^-8 dollars - 1 USD = 100 cents = 10^8 USD_MICROCENTS - Max int64 ≈ 9.22e18 USD_MICROCENTS ≈ $92.2B TOKENS are integer token counts. CREDITS/RISK_POINTS are generic integer units (optional in v0 implementations).
    - `amount` integer, required
  - `metadata` object

## Response `200`

Decision result

- object
  - `decision` 'ALLOW' | 'ALLOW_WITH_CAPS' | 'DENY', required
  - `caps` object: Optional soft-landing constraints returned by /decide or reservation ALLOW_WITH_CAPS. v0 intentionally keeps caps simple and concrete (no condition language). PRECEDENCE RULES: - If tool_allowlist is non-empty, ONLY those tools are allowed (denylist ignored). - Otherwise, if tool_denylist is non-empty, all tools EXCEPT those are allowed. - If both are empty/null, no tool restrictions apply. - Tool names are case-sensitive and match Action.name exactly.
    - `max_tokens` integer
    - `max_steps_remaining` integer
    - `tool_allowlist` string[]
    - `tool_denylist` string[]
    - `cooldown_ms` integer
  - `reason_code` string: 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_ms` integer
  - `affected_scopes` string[]: Canonical scope identifiers impacted by this decision, in canonical order.
  - `cycles_evidence` object: Reference to the CyclesEvidence envelope emitted for an operation. The `evidence_id` is the sha256 content hash of the JCS-canonical envelope and is computed SYNCHRONOUSLY at decision time, so it is returned on the operation response even though the envelope is Ed25519-signed and stored ASYNCHRONOUSLY. A caller (e.g. an APS gateway) binds its own signed receipt to this evidence by recording `evidence_id`, then fetches the envelope at `cycles_evidence_url` (see `getEvidence`) to verify it offline. Because signing/storage is async, the envelope MAY be served shortly after this response — consumers SHOULD treat a transient `404` from `getEvidence` as not-yet-available and retry. TRANSPORT METADATA, NOT ATTESTED: `cycles_evidence` is added to the response for the caller's convenience and is NOT part of the evidence the envelope attests. `remaining_ttl_ms` on ReservationCreateResponse is likewise a volatile transport observation rather than attested decision data, so a same-key replay can safely recompute it. The `evidence_id` is computed over the operation response WITHOUT either transport-only field — the envelope's `payload.<artifact>.response` mirror (`cycles-evidence-v0.2.yaml`) keeps `additionalProperties: false` and omits them. Implementations MUST compute `evidence_id` before stamping transport-only fields onto the response, and MUST NOT include either field in the attested payload.
    - `evidence_id` string, required: sha256 content id of the emitted envelope; resolve via getEvidence.
    - `cycles_evidence_url` string, uri, required: Absolute URL of the signed envelope, formed as `{server_id}/evidence/{evidence_id}`. Note `server_id` is already the canonical deployment base INCLUDING the `/v1` prefix (e.g. `https://cycles.example.com/v1`), so the join adds only `/evidence/{evidence_id}` — it MUST NOT re-add `/v1`.

## Other responses

- `400`: Error response
- `401`: Error response
- `403`: Error response
- `409`: Error response
- `500`: Error response

---

[API](https://skmtc.dev/runcycles/apis/cycles-budget-authority-api.md) · [All operations](https://skmtc.dev/runcycles/apis/cycles-budget-authority-api/llms.txt) · [OpenAPI document](https://skmtc.dev/runcycles/apis/cycles-budget-authority-api/revisions/7d42267c1ea0?raw)
