Approvals API
Approvals are decision requests with named approvers, a policy, and a frozen snapshot of what is being decided. See the Approvals user guide for the product behaviour.
All routes use API key authentication and run
with the key owner’s live permissions. An approval is visible to its requester,
its approvers, and holders of the approval:manage capability; anything else
returns 404.
List approvals
Section titled “List approvals”GET /api/v1/organizations/:orgId/approvals?tab=waiting
The tab query parameter selects the view:
waiting(default): pending approvals waiting on the caller’s decision.assigned: every approval the caller has ever been assigned.requested: approvals the caller raised.all: every approval in the organisation. Requiresapproval:manage. Supportsstatus,subjectType, andsubjectIdfilters.
limit (max 200) and offset page the results.
Read an approval
Section titled “Read an approval”GET /api/v1/approvals/:id
Returns the approval with its snapshot summary rows, resolved entity links,
per-approver assignments (including decisions and comments), the full audit
timeline, and a canDecide flag for the caller.
For budget change orders, terminal decisions also close the current proposal after processing. Live budget amounts remain unchanged on rejection, requested changes, expiry or cancellation. Read both records to confirm the result; see change-order outcomes.
Purchase cost visibility
Section titled “Purchase cost visibility”Purchase-order and supplier-bill approval amounts require finance:view_cost
in the project/client scope recorded on the request. This is independent of
finance:view_prices. A cost-only caller receives the cost summary and snapshot.
Without cost authority, the title is general, description and subjectFacts
are absent, and subjectSnapshot contains only nonfinancial summary rows. Its
data and renderedMarkdown are absent. This also applies to older requests.
The same projection applies to lists and mutation replies. The named MCP
list_approvals, get_approval, create_approval, decide_approval and
cancel_approval tools apply the same permissions. Approval visibility and
decision authority remain separate from cost visibility.
Record a decision
Section titled “Record a decision”POST /api/v1/approvals/:id/decisions
{ "decision": "approved", "comment": "Looks good" }decision is approved, rejected, or changes_requested; comment is
optional except for changes_requested, where it is required
(400 COMMENT_REQUIRED). The first decision per approver is final: repeating
the same decision returns the recorded result (idempotent, including after
the approval has closed), while a conflicting one returns
409 ALREADY_DECIDED. A decision on a closed approval returns
409 APPROVAL_CLOSED, on a reassigned-away assignment
409 NO_LONGER_REQUIRED, and on a stage that is not yet (or no longer)
active 409 STAGE_NOT_ACTIVE.
The policy resolves the request automatically: any completes on the first
response, all on unanimity (one rejection rejects), and quorum when the
configured number of approvals is reached or can no longer be reached. A
single changes_requested decision closes the request under every policy.
On a staged approval, only the active stage’s approvers can decide; an
approved stage advances the request to the next stage, and the request
completes when the final stage approves.
Cancel and reassign
Section titled “Cancel and reassign”POST /api/v1/approvals/:id/cancel withdraws a pending request. Allowed for
the requester and approval:manage holders. Optional reason.
POST /api/v1/approvals/:id/reassign moves one pending assignment to another
member. Requires approval:manage.
{ "fromAssignmentId": "…", "toUserId": "…" }Create a standalone approval
Section titled “Create a standalone approval”POST /api/v1/organizations/:orgId/approvals (requires approval:manage)
{ "title": "Approve the Q4 media budget", "approvers": [ { "kind": "user", "id": "…" }, { "kind": "group", "id": "…" } ], "policy": { "type": "quorum", "quorum": 2 }, "subjectSnapshot": { "summary": [{ "label": "Amount", "value": "$120,000" }] }, "entityLinks": [{ "type": "project", "id": "…", "label": "Q4 Media" }], "dueAt": "2026-09-15T00:00:00Z"}Groups expand to their current active members when the approval is created. An empty expansion is rejected rather than auto-approving.
Instead of a flat approvers + policy pair, pass stages, an ordered array
(max 10) of { "approvers": […], "policy": { … } } objects. Stages decide
sequentially; approvers in later stages are notified when their stage becomes
active. One user cannot appear in more than one stage.
Standing delegations
Section titled “Standing delegations”PUT /api/v1/organizations/:orgId/approval-delegations/me sets the caller’s
delegation:
{ "delegateUserId": "…", "endsAt": "2026-09-15T00:00:00Z" }While active, new assignments that would go to the caller go to the delegate
instead (recorded with delegatedFromUserId), unless the delegate is already
an approver on the same request, in which case the substitution is skipped.
Delegations never chain. GET …/me reads it,
DELETE …/me revokes it, and GET /api/v1/organizations/:orgId/approval-delegations
lists all live delegations (requires approval:manage).
Approval gates
Section titled “Approval gates”GET /api/v1/organizations/:orgId/approval-policies lists the configurable
gates (currently project.draft_activation) with their approvers.
PUT /api/v1/organizations/:orgId/approval-policies/:gateKey configures one:
{ "enabled": true, "policyType": "any", "dueHours": 48, "autoApply": true, "expiryBehavior": "expire", "approvers": [{ "kind": "group", "id": "…" }]}autoApply: accepted for compatibility and always stored astrue. On approval, Runnit performs the gated transition on the requester’s behalf. If a lifecycle guard still blocks it, the failure is audited on the approval and the requester is notified; the approval remains valid for a manual retry. A request raised by one of the gate’s approvers records their approval as it is created (not under segregation of duties).expiryBehavior: whatdueHourspassing does.expire(default),auto_approve, orauto_reject.clientOrganizationId: include it to create or update a client-scoped override instead of the org default. Resolution is most specific wins, so a client row (enabled or disabled) beats the default for that client’s projects.DELETE …/approval-policies/:gateKey?clientOrganizationId=…removes an override; without the parameter it removes the default.
All of these require approval:manage.
Inbound decision webhook
Section titled “Inbound decision webhook”For decisions made in an external system (Microsoft Power Automate, Logic Apps, or anything that can send signed HTTP):
PUT /api/v1/organizations/:orgId/approval-webhooks/generic with
{ "enabled": true, "rotateSecret": true } enables the receiver and returns
the signing secret ONCE (afterwards only a hint is readable). Requires
approval:manage.
The external system then posts to POST /api/approval-webhooks/generic (no
API key; authentication is the signature):
{ "organizationId": "…", "approvalRequestId": "…", "approverEmail": "director@example.com", "decision": "approved", "comment": "Signed off in Teams"}with headers X-Approval-Timestamp (unix seconds, within 5 minutes) and
X-Approval-Signature: sha256=<hex HMAC-SHA256 of "{timestamp}.{rawBody}">
computed with the signing secret. The approver email must belong to an active
member of the organisation; the decision then follows exactly the same rules
as any other surface.
Project activation approval
Section titled “Project activation approval”With the project.draft_activation gate enabled, moving a draft project to an
active status returns 409 with a machine-readable code:
APPROVAL_REQUIRED: no approval exists yet. Create one withPOST /api/v1/projects/:id/request-activation-approval(no body); the gate’s configured approvers are notified.APPROVAL_PENDING: a request is open;details.approvalRequestIdlinks to it.
Once approved, retry the status change. The approval is consumed atomically with the transition: one approval authorises one activation.