Approvals API
Approvals brings together all approvals and confirmations in two parts: (1) EntityApproval — persisted multi-approver records with configurable strategies (ALL, ANY, MAJORITY, QUORUM), approval groups and configs, for changes, change templates, incidents (closure/data breach) and workflow approval steps; (2) the union inbox (GET /api/approvals/inbox), which merges pending decisions from several areas into one list (absences, asset handovers, change-task readiness and more).
Supported Entity Types
| Entity-Type | Description | Status Effect |
|---|---|---|
CHANGE |
Change approval (multi-approver) | → APPROVED / REJECTED |
INCIDENT |
Incident closure and reopen (incl. GDPR data breach) | → CLOSED / RESOLVED |
CHANGE_TEMPLATE |
Change template approval | → APPROVED / REJECTED |
WORKFLOW |
Approval steps of running workflows (approver right: workflows.completeSteps) | → step APPROVED / REJECTED |
Approval steps in workflows can be decided automatically: auto-approve and auto-reject conditions check a field value of the workflow data, a permission or a role; multiple conditions are combined with AND or OR. Auto-reject is checked before auto-approve. A step gets its deadline via amount and unit (dueTimeAmount, dueTimeUnit); this yields the due date of the approval. A manual decision goes through the instance's approve endpoint (APPROVED or REJECTED, plus an optional comment); such a step cannot be completed instead. That way every decision takes the same path — same permission check, same trail, same rejection handling. The comment then appears in the running workflow under the step outputs. Configuration and examples: Workflows API →
Cross-Domain Pending Inbox (union inbox)
Beyond EntityApproval records, the union inbox (GET /api/approvals/inbox) collects pending decisions from several domains into one unified list. Each source returns only entries the caller may see; without the permission for a source it stays empty (no 403). Assigned WORK (tickets, incidents, problems, changes, change tasks, workflow steps) is separate and runs through /api/my-tasks.
| Provider | Source |
|---|---|
entityApproval | EntityApproval records (change/incident/template) |
absence | Absence requests (manager decision) |
handover | Asset handover confirmations |
changeTaskReady | Change-task "ready" confirmations |
cascadingAwareness | Cascading awareness notices |
Endpoints Overview
Approvals (User-Facing)
| Method | Endpoint | Description |
|---|---|---|
GET | /api/my-tasks | Assigned work items (not decisions) — documented in the API overview |
GET | /api/my-tasks/count | Count of assigned work items per scope (for badges) |
GET | /api/approvals/inbox | Union inbox of pending approvals across all sources (?types= CSV, ?limit 1–200 [default 50], ?offset) |
GET | /api/approvals/inbox/count | Counts per source (badge/tabs) |
POST | /api/approvals/inbox/decide | Universal decision via the inbox ID (TYPE:id) |
GET | /api/approvals/check | Checks whether an entityType/subType requires approval (configuration data for create and close dialogs) |
POST | /api/approvals/:id/decide | Make decision (approve/reject) |
The union inbox has no permission of its own. Each source checks its own permissions, so end users see their confirmation tasks too. Anyone without permission for any source gets an empty list. Invalid input is rejected with 400: an unknown types value, and a non-numeric limit or one outside 1–200. Likewise /check requires a valid entityType.
The approvers and comments of an entity come from that entity's own detail payload (change, incident, problem) — the visibility rules of that entity apply there.
Approval Groups (Admin)
| Method | Endpoint | Description |
|---|---|---|
GET | /api/admin/approval-groups | All approval groups |
POST | /api/admin/approval-groups | Create new approval group |
PATCH | /api/admin/approval-groups/:id | Update group (partial) |
DELETE | /api/admin/approval-groups/:id | Delete group |
GET | /api/admin/approval-groups/:id/members | Group members |
GET | /api/admin/approval-groups/:id/eligible-members | Eligible members (not yet in group) — ?search=, ?take/skip; returns {data, total} |
POST | /api/admin/approval-groups/:id/members | Add member |
PATCH | /api/admin/approval-groups/:id/members/:memberId | Update member (e.g. required flag) |
DELETE | /api/admin/approval-groups/:id/members/:memberId | Remove member (memberId = entry ID) |
Approval Configs (Admin)
| Method | Endpoint | Description |
|---|---|---|
GET | /api/admin/approval-configs | All approval configurations |
GET | /api/admin/approval-configs/:id | Config details |
POST | /api/admin/approval-configs | Create new config |
PUT | /api/admin/approval-configs/:id | Update config |
DELETE | /api/admin/approval-configs/:id | Delete config |
Configuration: Groups, Configs & Closure Policy
The configuration layer (approval groups + approval configs) is managed in the UI under Admin Center → Service Configuration → Approvals (/admin/approvals). The area has three tabs: Groups, Configurations and Closure Policy (= incident closure policy). All admin endpoints require a user login; API keys are not accepted.
Approval Group — Fields
| Field | Type | Description |
|---|---|---|
name | String (1–50) | Technical name, lowercase alphanumeric with hyphens |
displayName | String (1–100) | Display name |
description | String? (max. 500) | Description |
type | enum | CAB, EMERGENCY_CAB, INCIDENT_CLOSURE, INCIDENT_REOPEN, MAJOR_INCIDENT, DATA_PROTECTION, CUSTOM |
defaultStrategy | enum | ANY, ALL, QUORUM, MAJORITY |
defaultQuorum | Int? | Minimum count for QUORUM |
isActive | Boolean | Active/inactive (PATCH only) |
members[] | — | userId + weight (1–10) + isBackup. eligible-members lists only users holding the required approver permission. |
POST and PATCH validate the body strictly: a field not listed in this table is rejected with 400. name is set only on creation; members are managed via the members endpoints. On PATCH an omitted field stays unchanged; null clears description and defaultQuorum.
Approval Config (Closure Policy) — Fields
An approval config defines WHETHER and HOW an entity must be approved. For INCIDENT with a closure-related subType this is exactly the "Closure Policy". At most one config exists per (entityType, subType) (unique).
| Field | Type | Description |
|---|---|---|
entityType | enum | CHANGE, INCIDENT, PROBLEM, CHANGE_TEMPLATE, WORKFLOW |
subType | String? | e.g. CLOSURE, DATA_BREACH, EMERGENCY, MAJOR, P1, REOPEN … (refines the rule) |
requiresApproval | Boolean | Default true. false → no approval needed (closure without sign-off) |
approvalGroupId | cuid? | Which group approves |
strategy / quorum | enum? / Int? | Override; null = use group default |
autoAssign | Boolean | Default true — auto-assign approvers from the group |
conditions | JSON? | Condition matching, e.g. {"riskLevel":["HIGH","VERY_HIGH"]} |
priority | Int (0–100) | Higher = checked first (when multiple configs match) |
isActive | Boolean | Active/inactive |
Incident Closure Policy: The Closure Policy tab configures for INCIDENT whether closing requires sign-off (requiresApproval), which group + strategy applies and optional conditions. Approvers need incidents.approveClosure; the DATA_BREACH subType requires incidents.acknowledgeDataBreach instead (GDPR). Multiple approvals on the same entity (e.g. closure + data breach) run in parallel.
Incident Reopen Approval: Reopening a closed incident requires approval by default (reopenRequiresApproval). It runs through a dedicated approval group of type INCIDENT_REOPEN resp. the subType REOPEN and through its own approver permission incidents.approveReopen. This keeps the reopen approval separate from the closure approval, so that "may close" and "may reopen" can be different groups of people. The reopen goes through POST /api/incidents/:id/reopen; after sign-off (decision via POST /:id/decide) the incident is set to ACKNOWLEDGED, on rejection it stays CLOSED. reopenOverride NEVER bypasses the approval requirement. Reopen & Lifecycle
Approval Strategies
| Strategy | Description | Result |
|---|---|---|
ALL |
All approvers must approve | APPROVED when all approve, REJECTED on any rejection |
ANY |
A single approver is sufficient | APPROVED on first approval |
MAJORITY |
Simple majority (>50%) | APPROVED when >50% approve |
QUORUM |
Configurable minimum count | APPROVED when quorum count is reached |
API Examples
Get My Pending Decisions (cross-domain)
GET /api/approvals/inbox?types=CHANGE,HANDOVER_CONFIRM&limit=50&offset=0
Aggregated across all providers; each entry carries its source type and states whether it can be decided inline (decideMode: inline) or points to its own page (deeplink). Example (abbreviated):
{
"data": [
{
"itemId": "CHANGE:approval-uuid-1",
"type": "CHANGE",
"entityId": "change-uuid",
"entityNumber": "CHG-2026-000042",
"title": "Upgrade PostgreSQL",
"decideMode": "inline",
"deeplinkUrl": "/changes/change-uuid",
"inlineActions": { "rejectRequiresReason": true },
"initiatedAt": "2026-03-17T10:00:00Z",
"initiatedBy": { "id": "user-uuid", "name": "Jane Smith" },
"priority": "HIGH"
},
{
"itemId": "HANDOVER_CONFIRM:handover-uuid",
"type": "HANDOVER_CONFIRM",
"entityId": "handover-uuid",
"entityNumber": "HO-00007",
"title": "Dell XPS 15 Laptop",
"decideMode": "deeplink",
"deeplinkUrl": "/assets/handovers/handover-uuid",
"initiatedAt": "2026-03-17T14:00:00Z"
}
],
"pagination": { "total": 2, "limit": 50, "offset": 0 },
"counts": { "total": 2, "byType": { "CHANGE": 1, "HANDOVER_CONFIRM": 1 } }
}
Decisions go through POST /api/approvals/inbox/decide with exactly that itemId ({ itemId, decision: "APPROVE" | "REJECT", reason?, comment? }) — the aggregator routes by prefix to the right provider. Items with rejectRequiresReason: true require a reason on REJECT.
Make Decision
POST /api/approvals/:id/decide
{
"decision": true,
"comment": "Approved. Implementation plan looks solid."
}
Response
{
"approval": {
"id": "approval-uuid-1",
"entityType": "CHANGE",
"entityId": "change-uuid",
"decision": true,
"decisionAt": "2026-03-17T11:30:00Z",
"comment": "Approved. Implementation plan looks solid."
},
"evaluation": {
"isComplete": true,
"outcome": "APPROVED",
"approvals": 2,
"rejections": 0,
"pending": 0,
"total": 2,
"requiredForApproval": 2,
"strategy": "ALL"
}
}
When evaluation determines all required approvals are complete, the entity status is automatically updated (e.g., Change → APPROVED, Incident → CLOSED).
Check Whether Approval Is Required
GET /api/approvals/check?entityType=INCIDENT&subType=CLOSURE
{
"requiresApproval": true,
"strategy": "ALL",
"approvalGroupId": "clx-group-incident-closure"
}
This endpoint returns the configuration metadata for create/close dialogs. The live approval state of a specific entity is carried by its detail payload (e.g. GET /api/changes/:id).
Rejection Handling
For approval steps in workflows, the step configuration defines what happens after a rejection:
| Setting | Behavior |
|---|---|
failOnReject (default: on) | The workflow ends with status FAILED unless an escalation step is configured; the initiator is notified. |
failOnReject off | The workflow continues with the next steps. |
escalationStepId | Applies to every rejection — manual or automatic — and takes precedence over failOnReject: instead of failing, the specified step starts. Whoever handles that step is notified; if the system runs it, there is no notification, and if no handler can be determined it goes to the initiator. |
Permissions
| Permission | Description |
|---|---|
approvals.decide | Record decisions (on top of the assignment and the entity-specific approver right) |
approvals.viewGroups | View approval groups (settings tab Groups) |
approvals.manageGroups | Create/update/delete approval groups |
approvals.manageMemberships | Manage group members (add/update/remove) |
approvals.viewConfigs | View approval configs + closure policy (settings tabs Configs/Closure Policy) |
approvals.manageConfigs | Manage approval configs + closure policy |
Who may decide: A decision (POST /:id/decide resp. /inbox/decide) requires three things: (1) the approvals.decide permission; (2) the decision is assigned to the caller (else 403 NOT_ASSIGNED); (3) the approver permission for the entity type — INCIDENT → incidents.approveClosure, INCIDENT:DATA_BREACH → incidents.acknowledgeDataBreach, INCIDENT:REOPEN → incidents.approveReopen, CHANGE/CHANGE_TEMPLATE → changes.approve, WORKFLOW → workflows.completeSteps. For critical actions permissions are checked against the current database state, so a just-revoked right takes effect immediately. The requester cannot approve their own request. The detail page and the union inbox apply identical checks.
Error Handling
| Error | HTTP | Description |
|---|---|---|
APPROVAL_NOT_FOUND | 404 | Approval ID does not exist |
NOT_ASSIGNED | 403 | Approval is not assigned to the caller |
ALREADY_DECIDED | 409 | Decision already made |
REJECTION_COMMENT_REQUIRED | 400 | Rejection requires a comment |
NO_VALID_APPROVERS | 400 | No valid approvers configured |
GROUP_IN_USE | 409 | Group is used in active configs |
Change approvals in detail
Incident closure approval & GDPR