GET /api/v1/approvals
List approval requests filtered by status.Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
status | string | "pending" | Filter by status: "pending", "approved", "rejected" |
Response
POST /api/v1/approvals
Create a new approval request.Request Body
| Field | Type | Required | Description |
|---|---|---|---|
entityType | string | Yes | Entity type (e.g., "qualificationRule", "contactPolicy", "decisionFlow", "offer") |
entityId | string | Yes | ID of the entity to modify |
entityName | string | Yes | Display name of the entity |
action | string | Yes | "create", "update", or "delete" |
payload | object | No | The proposed changes to apply on approval |
stages | Array<{ stageName, requiredRole }> | No | Optional multi-stage chain. Each stage has stageName: string and requiredRole: "viewer" | "editor" | "admin". Decisions walk the stages in order. When omitted, one default admin stage is created. See Four-eyes governance. |
Example
201 Created
GET /api/v1/approvals/
Get a single approval request.POST /api/v1/approvals/
Resolve an approval request (approve or reject). Editors and admins can decide; the per-stagerequiredRole is then enforced inside the
walk. Viewers are 403’d at the route boundary.
When the last stage of the chain is approved, the stored payload
is automatically applied to the target entity. Supported entity types
for auto-application: algorithmModel, qualificationRule,
contactPolicy, decisionFlow, offer, creative, channel,
rankingProfile. An unsupported entityType causes the whole
transaction (stage update + parent flip + apply) to roll back —
returning a 409 — so an approval cannot flip to approved without the
entity actually changing.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
action | string | Yes | "approve" or "reject" |
comments | string | No | Reviewer comments |
Response (approved)
Response (rejected)
Roles
| Endpoint | Allowed Roles |
|---|---|
GET /approvals | any authenticated |
POST /approvals | any authenticated |
GET /approvals/{id} | any authenticated (returns parent + ordered stages array in one round-trip) |
POST /approvals/{id} (resolve) | editor or admin (per-stage requiredRole is enforced inside the walk) |
Auto-expiry
Pending approvals that sit unresolved for longer thanAPPROVAL_MAX_AGE_HOURS (default 168 = 7 days) are auto-flipped to
status = "expired" by the
/api/v1/cron/approvals-expire
endpoint. Wire it into your scheduler at any cadence — every 15 minutes
is plenty since the operation is a single bulk update and idempotent.
The cutoff is configurable per-deployment via the
APPROVAL_MAX_AGE_HOURS env var. Expired requests retain their full
audit trail; only status and resolvedAt change.
Four-eyes decision-flow publish gate
When a tenant setsrequirePublishApproval: true
(tenant settings), publishing a decision flow
requires a dedicated approval request:
| Field | Value |
|---|---|
entityType | "decisionFlow" |
action | "publish" |
entityId | the decision-flow id being published |
publish approval does not
mutate the flow when it is approved — it simply records the second-person
sign-off. The publish route checks for the
latest approved request matching this shape, and one approval authorizes exactly
one publish (the publish stamps the approval’s id onto the new version so it
can’t be reused). Because stage walking rejects self-approval and duplicate
approvers, an approved publish request guarantees two distinct identities.
See also: Compliance · Cron jobs