---
title: Approvals
description: "A definition says who approves a record in how many stages; a request walks them one at a time; one rejection ends it. What each refusal means."
---

Every endpoint here needs the `approvals` module. A definition is written once; requests are submitted against records and move through its stages.

## A definition is up to three stages

[`POST /approval-definitions`](/reference/approvals/post-api-v1-approval-definitions) takes a `label`, the `objectId` it applies to, a `version`, and `stages` — **one to three of them**, each naming exactly one approver:

```json
{ "approverUserId": "…", "label": "Manager" }
```

**Every approver must be a member of the organization.** A user id that is not answers `approver_outside_organization`, and the whole definition is refused rather than created with a gap.

## Submitting pins the record

[`POST /approvals`](/reference/approvals/post-api-v1-approvals) takes a `definitionId`, a `recordId` and the record's `revision`.

**It snapshots what the approvers will read.** The request stores the `recordRevision`, a `snapshot` of the record and its relations, and a `contentHash`. Approvers decide on that snapshot, not on whatever the record has become since.

Sending a stale revision is refused — see [revisions](/guides/concepts/revisions) for why that is not resolved to the newest.

**A submitter cannot be an approver.** If the submitter's own user id appears in any stage of the definition, the submission is refused with `approver_is_submitter`. The check is at submission, not at decision time, so a definition that names you is simply unusable by you.

## Deciding walks one stage at a time

[`POST /approvals/{id}/decisions`](/reference/approvals/post-api-v1-approvals-by-id-decisions) takes `decision` — `approve` or `reject` — the `stage` number, and an optional `comment` of up to 4,000 characters.

**The stage number is required and checked.** A request carries an `activeStage`, and a decision whose `stage` is not the active one is refused with `approval_stage_moved`, as is any decision on a request that is no longer `pending`. Read the request before deciding rather than assuming which stage is open.

**Only the assigned approver may decide.** The approver of stage _n_ is `stages[n-1].approverUserId`, and anyone else — including the submitter — gets `approver_not_assigned`.

A request's `state` is one of four, and the transitions are not a graph worth drawing:

| Decision | Result |
| --- | --- |
| `reject` at any stage | `rejected`. **One rejection ends the request**, whatever stages remain |
| `approve` at the last stage | `approved` |
| `approve` at an earlier stage | still `pending`, with `activeStage` advanced by one |

## Withdrawing

[`POST /approvals/{id}/withdraw`](/reference/approvals/post-api-v1-approvals-by-id-withdraw) sets the state to `withdrawn`.

**Only the submitter may withdraw**, and only while the request is `pending`: anyone else gets `withdraw_not_submitter`, and a request already decided gets `approval_not_pending`.

## Approvals block other work

A pending approval is not only a record of intent. **Editing the record it was submitted against is refused** with `record_approval_pending` — withdraw the approval first. A quote with one open likewise refuses to be revised, as `quote_approval_pending`. Submitting for approval is therefore closer to taking a lock on the thing being approved than to sending a notification.

## Four scopes, not one

| Scope | What it covers |
| --- | --- |
| `approvals.read` | Listing definitions, requests and decisions |
| `approvals.manage` | Creating a definition. Admin only |
| `approvals.submit` | Submitting **and withdrawing** — the two travel together |
| `approvals.decide` | Deciding a stage |

The split mirrors the rule above: submitting and deciding are different scopes, so a client built to file requests cannot approve them even before the per-request check runs.

## Reading

[`GET /approvals`](/reference/approvals/get-api-v1-approvals) lists requests and [`GET /approvals/{id}/decisions`](/reference/approvals/get-api-v1-approvals-by-id-decisions) answers the decisions made on one, each with its `stage`, `decision`, `comment` and `decidedBy`. A rejected request keeps its decisions, so the reason survives the refusal.
