Skip to main content
Dolfin owns the match. A purchase order, the bill, and (in v1.1) a goods received note are first-class Dolfin objects tied together by a line-level allocation layer and bundled for review into a match group. You store pointers to our ids (billId, poNumber, poId); the optional externalRef on each object is the reverse pointer back to your own id. v1.0 ships the two-way match (purchase order ↔ bill): price-vs-PO variance, resolution, and the approval gate. Receipt-quantity variance (goods received notes) is an additive v1.1 follow-up - no v1.0 route or payload changes when it lands.
All requests below require the x-dolfin-api-key and x-dolfin-organisation-id headers. See Authentication.

Concepts

ObjectWhat it is
Purchase orderThe order you raised. Lines carry an optional sku, a quantityOrdered, and a net subTotal.
Match groupThe review unit for one bill: its allocations plus derived variances. Status is Open, Variance, or Resolved.
AllocationOne row tying a bill line to a PO line, on net. The primitive that absorbs every match edge.
VarianceComputed at read time, never stored. v1 surfaces price variance (billed vs ordered unit price).

Lifecycle

1

Raise a purchase order

POST /purchase-orders with the supplier and its lines.
2

Dolfin matches it to the bill

When a bill for the same supplier is in review, Dolfin lazily creates a match group and aligns lines: exact sku matches auto-allocate, close description matches are surfaced as suggestions to confirm, and anything else is allocated manually. The PO and the bill can arrive in either order.
3

A variance surfaces

A price variance outside the organisation’s rounding tolerance moves the group to Variance.
4

A reviewer resolves it

accept_price (or dispute, …) settles the match. A clean or accepted match becomes Resolved.
5

The gate clears

A bill cannot leave review for approval while its match group is unresolved. Matching never moves or pays the bill itself - it produces a fact your approval policy consumes.

Create a purchase order

curl -X POST https://api.dolfinai.co/purchase-orders \
  -H "x-dolfin-api-key: $DOLFIN_API_KEY" \
  -H "x-dolfin-organisation-id: $ORG_ID" \
  -H "content-type: application/json" \
  -d '{
    "supplierId": "…",
    "poNumber": "PO-1001",
    "currency": "GBP",
    "lines": [
      { "description": "Widget A", "sku": "WID-A",
        "quantityOrdered": 10, "unitPrice": 5.00, "subTotal": 50.00, "sortOrder": 0 }
    ]
  }'

Read a bill’s match group

The derived view carries per-line billedNet / allocatedNet / priceVarianceTotal / hasPriceVariance, fuzzy suggestions for unmatched lines, and the group status.
# By bill (404 until a match group exists):
curl https://api.dolfinai.co/bills/{billId}/match-group \
  -H "x-dolfin-api-key: $DOLFIN_API_KEY" -H "x-dolfin-organisation-id: $ORG_ID"

# Or by match-group id:
curl https://api.dolfinai.co/match-groups/{id} \
  -H "x-dolfin-api-key: $DOLFIN_API_KEY" -H "x-dolfin-organisation-id: $ORG_ID"

Allocate a bill line (or confirm a suggestion)

curl -X POST https://api.dolfinai.co/match-groups/{id}/allocations \
  -H "x-dolfin-api-key: $DOLFIN_API_KEY" -H "x-dolfin-organisation-id: $ORG_ID" \
  -H "content-type: application/json" \
  -d '{ "billLineId": "…", "poLineId": "…", "matchedQuantity": 10, "method": "Manual" }'
The double-allocation guard is enforced in the database: the total matched against any single PO line can never exceed its ordered quantity (HTTP 409 Match.OverAllocation). You cannot pay the same order twice.

Resolve

curl -X POST https://api.dolfinai.co/match-groups/{id}/resolve \
  -H "x-dolfin-api-key: $DOLFIN_API_KEY" -H "x-dolfin-organisation-id: $ORG_ID" \
  -H "content-type: application/json" \
  -d '{ "action": "AcceptPrice" }'
AcceptPrice, AcceptShort, and AcceptAndUpdateCatalog settle the match; Dispute and RequestCreditNote keep it blocked. AcceptAndUpdateCatalog only records the intent - it does not mutate any catalog.

Webhooks

Subscribe to match-group lifecycle changes like any other Dolfin webhook:
EventWhen
match_group.createda match group is created for a bill
match_group.variancea variance needs review
match_group.resolvedthe match settled (clean or resolved)
Each payload carries a matchGroup summary (id, billId, status, resolution); fetch GET /match-groups/{id} for the full derived view.

Notes

  • Allocate on net - tax is computed separately, so per-line tax rounding never shows up as a fake price variance.
  • The rounding tolerance (default 1p) is configurable per organisation and feeds both line reconciliation and the clean-match threshold.
  • “Synced to accounting” does not exist yet; there is no match_group.synced webhook in v1.