> ## Documentation Index
> Fetch the complete documentation index at: https://docs.dolfinai.co/llms.txt
> Use this file to discover all available pages before exploring further.

# Approval Policies

> Auto-approve trusted bills, void the ones you never want to pay, and route the rest to the right approver

An **approval policy** is a rule that decides, per bill, what happens the moment it's submitted for
review: **auto-approve** it, **route** it to named approvers, or **void** it outright. Policies are an
ordered, per-organisation list evaluated deterministically at the `PendingReview → …` transition — so
you can auto-approve the £9/mo SaaS subscription, force a second pair of eyes on anything over £500,
and always route a named supplier to a specific approver.

<Note>
  All requests below require the `x-dolfin-api-key` and `x-dolfin-organisation-id` headers. See
  [Authentication](/guides/authentication).
</Note>

## Where policies fit

Policies are evaluated inside [`POST /bills/{id}/submit-review`](/guides/quick-start-ap#step-3-submit-for-review),
after the supplier is resolved. The winning policy's **action** is the state the bill lands in:

| Action         | Bill lands in   | Use it for                                                                           |
| -------------- | --------------- | ------------------------------------------------------------------------------------ |
| `AutoApprove`  | `Approved`      | Trusted, low-risk bills — skips review entirely, ready for payment.                  |
| `AlwaysReview` | `NeedsApproval` | Bills that need a human — routed to the policy's approvers (or any admin).           |
| `AutoVoid`     | `Voided`        | Hard stops — a blocked supplier, an unsupported currency. Terminal, non-recoverable. |

With **no policies configured**, or when **no policy matches**, every bill with a resolved supplier goes
to `NeedsApproval` — the safe default. Deleting every policy can never silently start auto-paying bills.

## Concepts

| Field                 | What it is                                                                                                             |
| --------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| **`name`**            | A human label shown on the bill it applies to (`"High value bills"`).                                                  |
| **`priorityOrder`**   | `0`–`1000`, **lower wins**. Sorts the list and breaks ties between matching policies.                                  |
| **`active`**          | `false` skips the policy at evaluation without deleting it — the soft off-switch.                                      |
| **Conditions**        | `amountGte` / `amountLte` (+ `currency`), `supplierIds`, `supplierStanding`. All optional; omitted means "don't care". |
| **`action`**          | `AutoApprove`, `AlwaysReview`, or `AutoVoid`.                                                                          |
| **`approverUserIds`** | For `AlwaysReview`: who to notify. Empty ⇒ no designated approver, any admin can action it in-app.                     |

### Conditions

A bill matches a policy only if **every** set condition holds:

* **`amountGte` / `amountLte`** — inclusive bounds on the bill's `totalAmount`. Set both for a closed range
  (`amountGte: 0`, `amountLte: 300` ⇒ "£0–£300"). When either is set, **`currency` is required** and scopes
  the policy to that currency — there's **no FX conversion**, so a `500 GBP` policy simply won't match a USD
  bill (add a separate USD policy for that).
* **`supplierIds`** — a list of supplier UUIDs. **Empty ⇒ global** (any supplier). Otherwise the policy is
  supplier-specific.
* **`supplierStanding`** — `Any`, `New`, or `Established`. A supplier is `Established` once **any** of their
  bills has been submitted for review before (even if it was rejected or is still pending); `New` is a
  supplier you've never dealt with. The safe auto-approve combo reads `amountLte` + `supplierStanding: Established`.

### How the winner is chosen

Exactly **one** policy wins — actions are never merged — so the outcome is always predictable:

<Steps>
  <Step title="Supplier-specific beats global">
    If any policy that names this bill's supplier matches, the winner comes from that set and global
    policies are ignored.
  </Step>

  <Step title="Lowest priorityOrder wins">
    Within the chosen set, the policy with the lowest `priorityOrder` wins. Ties break by creation time
    (oldest first).
  </Step>
</Steps>

Because there's a single winner, "require approval beats auto-approve" is just a matter of giving the
`AlwaysReview` policy a lower `priorityOrder` than the broad `AutoApprove` one.

<Note>
  The decision is **snapshotted onto the bill** at submit time. Editing or deleting a policy never
  retro-changes a bill that's already in review — it keeps the policy name and reasoning it was evaluated
  against.
</Note>

## Create a policy

`POST /ap/approval-policies`. This example auto-approves bills up to £300 from suppliers you've dealt with
before:

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.dolfinai.co/ap/approval-policies \
    -H "x-dolfin-api-key: $DOLFIN_API_KEY" \
    -H "x-dolfin-organisation-id: $ORG_ID" \
    -H "Content-Type: application/json" \
    -d '{
      "name": "Auto-approve small trusted bills",
      "priorityOrder": 200,
      "active": true,
      "amountLte": "300.00",
      "currency": "GBP",
      "supplierStanding": "Established",
      "action": "AutoApprove"
    }'
  ```

  ```javascript Node.js theme={null}
  const policy = await fetch('https://api.dolfinai.co/ap/approval-policies', {
    method: 'POST',
    headers: {
      'x-dolfin-api-key': process.env.DOLFIN_API_KEY,
      'x-dolfin-organisation-id': process.env.ORG_ID,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      name: 'Auto-approve small trusted bills',
      priorityOrder: 200,
      active: true,
      amountLte: '300.00',
      currency: 'GBP',
      supplierStanding: 'Established',
      action: 'AutoApprove',
    }),
  }).then(r => r.json());
  ```
</CodeGroup>

**Response (`201 Created`):**

```json theme={null}
{
  "id": "p1111111-abcd-ef01-2345-6789abcdef01",
  "name": "Auto-approve small trusted bills",
  "priorityOrder": 200,
  "active": true,
  "amountGte": null,
  "amountLte": "300.00",
  "currency": "GBP",
  "supplierIds": [],
  "supplierStanding": "Established",
  "action": "AutoApprove",
  "approverUserIds": [],
  "createdAt": "2026-07-12T09:00:00Z",
  "updatedAt": "2026-07-12T09:00:00Z"
}
```

### More examples

**Force review on high-value bills, routed to the finance lead** (lower `priorityOrder`, so it beats the
auto-approve rule above):

```json theme={null}
{
  "name": "High value bills",
  "priorityOrder": 100,
  "active": true,
  "amountGte": "500.00",
  "currency": "GBP",
  "action": "AlwaysReview",
  "approverUserIds": ["u5678901-abcd-ef01-2345-6789abcdef01"]
}
```

**Always route a specific supplier to a named approver** (supplier-specific, so it beats any global rule):

```json theme={null}
{
  "name": "Landlord invoices → Priya",
  "priorityOrder": 50,
  "active": true,
  "supplierIds": ["s2222222-abcd-ef01-2345-6789abcdef01"],
  "action": "AlwaysReview",
  "approverUserIds": ["u3333333-abcd-ef01-2345-6789abcdef01"]
}
```

**Void bills in a currency you don't pay:**

```json theme={null}
{
  "name": "Reject USD bills",
  "priorityOrder": 10,
  "active": true,
  "amountGte": "0",
  "currency": "USD",
  "action": "AutoVoid"
}
```

## List, get, update, delete

<CodeGroup>
  ```bash List theme={null}
  curl https://api.dolfinai.co/ap/approval-policies \
    -H "x-dolfin-api-key: $DOLFIN_API_KEY" \
    -H "x-dolfin-organisation-id: $ORG_ID"
  ```

  ```bash Get theme={null}
  curl https://api.dolfinai.co/ap/approval-policies/p1111111-abcd-ef01-2345-6789abcdef01 \
    -H "x-dolfin-api-key: $DOLFIN_API_KEY" \
    -H "x-dolfin-organisation-id: $ORG_ID"
  ```

  ```bash Update theme={null}
  curl -X PUT https://api.dolfinai.co/ap/approval-policies/p1111111-abcd-ef01-2345-6789abcdef01 \
    -H "x-dolfin-api-key: $DOLFIN_API_KEY" \
    -H "x-dolfin-organisation-id: $ORG_ID" \
    -H "Content-Type: application/json" \
    -d '{
      "name": "Auto-approve small trusted bills",
      "priorityOrder": 200,
      "active": false,
      "amountLte": "300.00",
      "currency": "GBP",
      "supplierStanding": "Established",
      "action": "AutoApprove"
    }'
  ```

  ```bash Delete theme={null}
  curl -X DELETE https://api.dolfinai.co/ap/approval-policies/p1111111-abcd-ef01-2345-6789abcdef01 \
    -H "x-dolfin-api-key: $DOLFIN_API_KEY" \
    -H "x-dolfin-organisation-id: $ORG_ID"
  ```
</CodeGroup>

`GET /ap/approval-policies` returns the policies sorted by `priorityOrder`. `PUT` replaces the policy in
place — send the full policy body, as with create. Prefer setting `active: false` over deleting when you
want to keep the rule around; deleting is safe either way, since bills already in review keep their
snapshot.

## Seeing which policy applied

Once a bill has been submitted, its response carries the decision so you can show it in your UI:

* **Auto-approved** — `state: "Approved"` with `approvedBy: null` (no human reviewed it) and the applied
  policy name and reasoning attached.
* **Routed for approval** — `state: "NeedsApproval"` with the resolved approver set the bill was routed to.
* **Auto-voided** — `state: "Voided"` with `voidedBy: null` and the reasoning as the void reason.

A `Voided` bill is terminal — re-upload to start again. A `Rejected` bill (a human decision in Step 4 of
the AP quick start) can be reopened instead.

## Next Steps

<CardGroup cols={2}>
  <Card title="Quick Start - AP" icon="file-invoice" href="/guides/quick-start-ap">
    The full bill lifecycle: import, review, submit, and approve.
  </Card>

  <Card title="Three-way Matching" icon="link" href="/guides/three-way-matching">
    Gate approval on purchase-order and delivery-note variances.
  </Card>
</CardGroup>
