Approval Workflows
An approval workflow gates a state transition (e.g., invoice finalization, publishing a price change) behind one or more human approval steps. Workflows are defined as templates (/v1/approval-workflows) and instantiated as approval requests (/v1/approvals/...) when a triggering action occurs on a governed entity.
Why this matters. As billing operations scale, individual write actions need governance - SOX compliance, internal financial controls, and dispute prevention all demand a review layer over high-impact mutations. Kontorion's approval workflows let you define multi-step templates with role-based approvers, per-step approval thresholds, timeouts, and configurable timeout/rejection handling, so the right humans are in the loop without slowing routine work. The same engine supports a finance team requiring CFO sign-off on invoice finalization, a catalog team routing price updates through review, an enterprise platform requiring two-person approval on subscription changes, and a regulated business gating tax-rule edits behind compliance review.
Concepts
| Object | Purpose |
|---|---|
ApprovalWorkflowTemplate | Reusable definition: entity type, trigger action, ordered steps, timeout/rejection handling. |
ApprovalStep | One step inside a template: who approves, how many approvals are required, timeout. |
ApprovalRequest | A live instance: which entity, current step, decisions so far, status. |
ApprovalDecision | One person's decision (APPROVED / REJECTED) on one step. |
Triggers
A template binds an entity_type to a trigger_action. When that action is attempted on an entity of that type, an approval request is created automatically and the action is held until the request resolves.
entity_type is one of (uppercase): PRODUCT, PLAN, PRICE, PRICE_FORMULA, PROMOTION, INVOICE, SCHEDULED_CHANGE, SUBSCRIPTION, CUSTOMER, TAX_RULE.
trigger_action is one of: CREATE, UPDATE, PUBLISH, FINALIZE, RELEASE, ACTIVATE, DELETE, VOID, PRICE_UPDATE.
For example, an INVOICE + FINALIZE template gates invoice finalization; a PRICE + PRICE_UPDATE template gates price changes.
Steps and decision rules
Each step has:
name- a human-readable label for the stepstep_number- the step's position in the ordered sequenceapprover_rolesand/orapprover_user_ids- who may decide on the step (at least one of the two must be non-empty)min_approvals- how many approvals are required to clear the step (integer, must be>= 1)timeout_hours- how long the step may stay pending before the template's timeout handling appliesnotify_on_start- whether to notify approvers when the step begins
Steps run sequentially; a rejected step rejects the whole request immediately (approval.request_rejected).
Template-level handling controls what happens at the edges:
on_timeout-ESCALATE,AUTO_APPROVE, orAUTO_REJECT(defaultAUTO_REJECT)on_rejection-REVERTorHOLD(defaultHOLD)
Lifecycle
Code
| Webhook | Fires when |
|---|---|
approval.request_created | A new request was instantiated |
approval.step_started | Workflow advanced to a new step |
approval.step_approved | Step reached its required min_approvals |
approval.step_auto_approved | Step timed out with on_timeout: AUTO_APPROVE |
approval.step_escalated | Step timed out with on_timeout: ESCALATE |
approval.request_approved | Final step approved; entity transition can now apply |
approval.request_rejected | An approver explicitly rejected |
approval.request_auto_rejected | Request timed out with no resolution |
approval.request_cancelled | Operator cancelled the request |
Creating a template
Code
Approving and rejecting
Approve and reject are two separate endpoints. Both accept an optional comment (the request body may be omitted entirely) and return the updated approval request.
Code
Escalation
When a step stays pending past its timeout_hours without reaching min_approvals, the template's on_timeout setting decides what happens. With on_timeout: ESCALATE, the runner escalates the step and emits approval.step_escalated. With AUTO_APPROVE or AUTO_REJECT, the request resolves automatically (approval.step_auto_approved or approval.request_auto_rejected).
Cancelling
Cancelling takes no request body and returns 204 No Content.
Code
Cancellation is appropriate when the underlying entity no longer needs the gated transition (e.g., the invoice was voided before approval). The emitted approval.request_cancelled event carries request_id and cancelled_by.
Next steps
- Invoices - the most common gated entity
- Webhooks - event types emitted by the runner
- API Reference - Approval Workflows