Approvals
The approval request lifecycle: an approver snapshot taken at creation, m-of-n, veto, and returning the reserve.
When the Policy Engine returns queue, the action isn't executed right away. Instead, it becomes an approval request and waits for the required number of approvals. This page covers how a request moves from creation to resolution. For who puts an action in the queue and why, see Policy Engine.
Where an approval request comes from
A state-changing action (a transfer, address deletion, a counterparty change) returns one of two responses:
200: the policy allowed the action (allow), and it was executed immediately.202: the policy queued the action (queue), and an approval request was created. The response contains its id.
The request carries a snapshot of the decision as of its creation: which policy made the decision (decisionPolicyId), who is allowed to approve, and what the threshold is. That's why later policy edits don't affect requests that already exist.
Lifecycle
| Status | Meaning | Reserve (for transfers) |
|---|---|---|
PENDING | awaiting votes | held in reserved |
APPROVED | threshold reached, action applied | debited |
REJECTED | vetoed by an approver, or stale | returned to available |
CANCELLED | cancelled by the initiator (transfers: /cancel) | returned to available |
How voting works (m-of-n)
Votes are cast by members of the request's approver group. The initiator can't vote on their own request (unless the policy explicitly allows it), and everyone gets one vote. For example, with a 2 of 3 threshold, two approve votes are needed; a single reject is a veto, and the request immediately becomes REJECTED.
Voting on a request:
curl -X POST "[BASE_URL]/api/v1/vaults/acme-otc/approval-requests/REQUEST_ID/approvals" \
-H "Authorization: Bearer $V3_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "decision": "approve" }'
await fetch("[BASE_URL]/api/v1/vaults/acme-otc/approval-requests/REQUEST_ID/approvals", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.V3_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ decision: "approve" }),
});
requests.post(
"[BASE_URL]/api/v1/vaults/acme-otc/approval-requests/REQUEST_ID/approvals",
headers={"Authorization": f"Bearer {os.environ['V3_TOKEN']}"},
json={"decision": "approve"},
)
The final approve that reaches the threshold applies the target action in the same request. The response contains the final status and a resolvedResultId (or an applyError if the action couldn't be applied).
There are two voting surfaces:
| Surface | Used for | Pending status |
|---|---|---|
approval-requests/{id}/approvals | any gated action | PENDING |
transfers/{id}/approvals | transfers only | awaiting_approval |
reject is a veto: a single rejection is enough to make the request REJECTED. The threshold is reached only with approve votes. Check the name of the vote field (decision here) in the API Reference.
Snapshots and race protection
Two mechanisms make the outcome predictable:
- Approver snapshot. A request remembers the approvers and threshold as they were at the time of creation. If the policy changes afterward, requests that are already open aren't affected.
- Optimistic lock. The final
approveapplies the action atomically. If the target object has changed since the request was created, the request is markedREJECTED(stale) instead of applying an outdated action. This rules out races between concurrent changes.
Composite requests
Several related actions can be combined into a single request with one approval flow:
POST /api/v1/vaults/{code}/approval-requests/compositecreates a chain of steps as a single entity. Each step is gated by its own policy; anapprovevote counts toward the steps the voter is authorized for, and the request becomesAPPROVEDonce every step reaches its own threshold. Arejectvetoes the entire chain. If all steps are allowed immediately, the chain is applied without a request (applied: true).
Where to find approval requests
GET /api/v1/vaults/{code}/approval-requestsreturns the requests visible to you (as the initiator, an approver, or an admin).GET /api/v1/vaults/{code}/approval-requests/{id}returns the details of a single request.
Feed filters:
| Filter | What it filters |
|---|---|
status | PENDING / APPROVED / REJECTED / CANCELLED |
action | action type |
mine=true | requests where I'm the initiator |
pendingMine=true | requests awaiting my vote |
from / to | time window by creation time |
resolvedFrom / resolvedTo | time window by resolution time |
initiatorId / approverId / decisionPolicyId | by participant and policy |
pendingMine=true answers the question "what's waiting for my vote specifically," which is handy for an approver's dashboard.