A destructive tool call in prod is held at the gateway until a human decides it. Control owns the approval policies that decide which calls are held and who may review them, and the inbox where reviewers vote. See Approvals for the model.
There is no endpoint that creates an approval request. Requests arrive from the gateway when a call is held; these routes read them and record votes.
Mutating routes re-read the workspace and the membership under the mutation lock before authorizing, so a role or freeze change that lands mid-request is seen.

Approval policy object

The operator_approval_policy contract, frozen in packages/proto/operator_approval_policy.schema.json.
integer
required
Currently 1.
string
required
Prefixed apy_.
string
required
Prefixed ws_.
string
required
Unique within the workspace.
string
required
active or disabled.
integer
required
Monotonic revision number, starting at 1. This is the value you pass as expected_revision.
string
required
Prefixed ayr_.
array
required
The stored, normalized rules. Every list inside a rule is deduplicated and sorted on store, so what you read back may not be ordered as you sent it.
string
required
RFC 3339 UTC.
string
required
RFC 3339 UTC.

Rule fields

string
required
1–64 characters matching ^[A-Za-z0-9][A-Za-z0-9_.:-]*$. Unique within the policy.
string
required
allow or deny.
array
required
Exactly one entry, "tool.call". The action must be named explicitly; there is no wildcard.
array
required
1–100 tool patterns. Each is at most 256 characters and supports literal names and * only — ?, [, ], and whitespace are rejected.
array
required
1–3 entries from dev, stage, prod.
array
required
Up to 2 entries from admin, owner. May be empty, but a rule must name at least one approver by role or by user id.
array
required
Up to 100 control user ids, each prefixed usr_ and at most 64 characters. Every id must be a current member of this workspace, or the request is rejected with 422.
integer
required
1 or 2. The number of approvals required.

List approval policies

GET /api/v1/workspaces/{workspace_id}/approval-policies Requires workspace.read. Returns every policy in the workspace, disabled ones included, ordered by name then id. Not paginated.
string
required
Workspace id, prefixed ws_.

Get an approval policy

GET /api/v1/workspaces/{workspace_id}/approval-policies/{policy_id} Requires workspace.read. Returns the approval policy object.
string
required
Workspace id, prefixed ws_.
string
required
Approval policy id, prefixed apy_.

Errors

Create an approval policy

POST /api/v1/workspaces/{workspace_id}/approval-policies Requires approval.policy.manage (owner). Returns 200 with the new policy at revision 1.
string
required
1–128 characters, and must not be blank after trimming. Unique within the workspace.
array
required
1–100 rules. See Rule fields.
integer
Must be omitted on create. Sending it is a 422.
Additional properties are rejected.

Errors

Rule-validation messages include rules must contain between 1 and 100 entries, rule fields are invalid, rule_id must be unique, effect must be allow or deny, actions must explicitly match tool.call, tools must not be empty, tool patterns support literal names and * only, environments are invalid, approver principals are invalid, and quorum must be 1 or 2.

Revise an approval policy

PUT /api/v1/workspaces/{workspace_id}/approval-policies/{policy_id} Requires approval.policy.manage (owner). Replaces the name and the whole rule set, creating revision current + 1. Returns 200 with the new revision.
string
required
Approval policy id, prefixed apy_.
string
required
1–128 characters, not blank after trimming.
array
required
1–100 rules. The full replacement set — this is not a patch.
integer
required
The revision you are replacing, at least 1. Required on update.

Errors

Disable an approval policy

POST /api/v1/workspaces/{workspace_id}/approval-policies/{policy_id}/disable Requires approval.policy.manage (owner). Sets status to disabled without deleting the policy or its revisions. Returns 200 with the policy object.
integer
required
The revision you observed, at least 1.
Changing or disabling an approval policy changes the workspace’s policy-set hash. Every pending or approved-but-unexecuted approval that was raised under the old set becomes invalidated and can no longer be voted on or executed.

Errors

Approval object

The operator_approval contract, frozen in packages/proto/operator_approval.schema.json.
string
required
Prefixed apr_.
string
required
Prefixed ws_.
string
required
The original request’s environment (dev, stage, prod), not a union of matching rule scopes.
string
required
The virtual key that made the held call, prefixed key_.
string
required
The inbound request, prefixed req_. Also the X-Request-Id.
string | null
required
The logical tool call, prefixed inv_, when one is bound.
string
required
The namespaced tool name the rules matched.
string
required
The namespaced tool name the invocation names.
string
required
Prefixed conn_.
object
required
Numeric context only — currently amount_usd. Never arbitrary argument names or values: raw tool arguments are not in this projection.
string
required
Why the call was held.
string
required
pending, approved, denied, expired, or invalidated. This is the effective status: a stored pending or approved request that is unexecuted and past expires_at reports expired, and one whose policy-set hash no longer matches the workspace’s current set reports invalidated.
string
required
awaiting_quorum, quorum_met, default_deny, reviewer_deny, expired, policy_changed, or execution_failed.
string
required
not_requested, pending, succeeded, or failed.
integer
required
The approval’s own optimistic-concurrency counter. Pass it as expected_revision when voting.
string
required
RFC 3339 UTC.
string
required
RFC 3339 UTC. After this, the approval can no longer be voted on or executed.
string | null
required
RFC 3339 UTC, or null.
string | null
required
RFC 3339 UTC, or null.
object
required
policy_set_hash plus the rules that matched, each carrying its policy_id, revision_id, rule_id, effect, actions, tools, environments, approver_roles, approver_user_ids, and quorum. This is the rule set as it stood when the call was held.
array
required
Every vote cast, each with user_id, decision, reason, and created_at.
object
required
receipt_id, error_code, and digest from the resumed execution, each null until it runs.
boolean
required
Whether the signed-in operator could vote right now. Informational only — control rechecks authorization under its row lock when the vote arrives.
string | null
required
not_pending, expired, frozen, role, policy_changed, not_eligible, or already_voted. null when can_vote is true.
object | null
required
The signed-in operator’s own vote (decision, reason, created_at), or null.
integer
required
Approvals needed.
integer
required
Approvals cast so far.

List approvals

GET /api/v1/workspaces/{workspace_id}/approvals Requires workspace.read. Keyset-paginated, newest first by (created_at, approval_id).
integer
default:"50"
1–50.
string
Opaque cursor from a previous next_cursor, at most 1024 characters. An undecodable cursor is a 422 ({"detail": "invalid cursor"}).
string
One of pending, approved, denied, expired, invalidated. Filters on the effective status, so pending excludes rows that have expired or been invalidated by a policy change, and expired includes stored-pending rows past their expiry.
array
required
Approval objects.
string | null
required
Pass back as cursor. null when the listing is exhausted.

Get an approval

GET /api/v1/workspaces/{workspace_id}/approvals/{approval_id} Requires workspace.read. Returns one approval object.
string
required
Approval id, prefixed apr_.

Errors

Vote on an approval

POST /api/v1/workspaces/{workspace_id}/approvals/{approval_id}/votes Requires approval.decide (admin or owner) and eligibility under the rules that matched when the call was held. Returns 200 with the updated approval object, including the recomputed quorum. The approval row is locked for the duration, so two reviewers racing to the last slot cannot both succeed.
string
required
approve or deny.
string
required
1–512 characters. Whitespace-only is rejected — a review that records nothing is not a review.
integer
required
The approval’s revision as you read it, at least 1.
Additional properties are rejected.

Errors

vote_block_reason on the approval object tells you in advance which of these a vote would hit. It is a hint for rendering, not an authorization result: control re-evaluates every condition under the row lock.