Skip to main content
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.
All requests below require the x-dolfin-api-key and x-dolfin-organisation-id headers. See Authentication.

Where policies fit

Policies are evaluated inside POST /bills/{id}/submit-review, after the supplier is resolved. The winning policy’s action is the state the bill lands in:
ActionBill lands inUse it for
AutoApproveApprovedTrusted, low-risk bills — skips review entirely, ready for payment.
AlwaysReviewNeedsApprovalBills that need a human — routed to the policy’s approvers (or any admin).
AutoVoidVoidedHard 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

FieldWhat it is
nameA human label shown on the bill it applies to ("High value bills").
priorityOrder01000, lower wins. Sorts the list and breaks ties between matching policies.
activefalse skips the policy at evaluation without deleting it — the soft off-switch.
ConditionsamountGte / amountLte (+ currency), supplierIds, supplierStanding. All optional; omitted means “don’t care”.
actionAutoApprove, AlwaysReview, or AutoVoid.
approverUserIdsFor 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.
  • supplierStandingAny, 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:
1

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.
2

Lowest priorityOrder wins

Within the chosen set, the policy with the lowest priorityOrder wins. Ties break by creation time (oldest first).
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.
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.

Create a policy

POST /ap/approval-policies. This example auto-approves bills up to £300 from suppliers you’ve dealt with before:
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"
  }'
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());
Response (201 Created):
{
  "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):
{
  "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):
{
  "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:
{
  "name": "Reject USD bills",
  "priorityOrder": 10,
  "active": true,
  "amountGte": "0",
  "currency": "USD",
  "action": "AutoVoid"
}

List, get, update, delete

curl https://api.dolfinai.co/ap/approval-policies \
  -H "x-dolfin-api-key: $DOLFIN_API_KEY" \
  -H "x-dolfin-organisation-id: $ORG_ID"
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"
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"
  }'
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"
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-approvedstate: "Approved" with approvedBy: null (no human reviewed it) and the applied policy name and reasoning attached.
  • Routed for approvalstate: "NeedsApproval" with the resolved approver set the bill was routed to.
  • Auto-voidedstate: "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

Quick Start - AP

The full bill lifecycle: import, review, submit, and approve.

Three-way Matching

Gate approval on purchase-order and delivery-note variances.