Skip to main content
Three-way matching answers one question before a bill is paid: did we order it, did we receive it, and were we charged the right amount? Dolfin owns the match across three first-class objects and gives you the derived result to act on. The match runs on two legs, both anchored on the bill:
  • Bill ↔ PO — the price leg. Billed unit price vs the ordered unit price, and billed quantity vs the ordered quantity (over-ordering).
  • Bill ↔ DN — the receipt leg. Billed quantity vs the quantity actually delivered (short/over delivery).
You never call a “match” endpoint. You feed Dolfin the POs and DNs, Dolfin bundles each bill’s match into a match group, and you read the derived view, confirm or correct the line-level allocations, and resolve any variances. POs and DNs are trusted platform data you push in; bills arrive through the normal AP review flow. You store pointers to our ids (billId, poNumber, poId, delivery-note ids); the optional externalRef on each object is the reverse pointer back to your own record.
All requests below require the x-dolfin-api-key and x-dolfin-organisation-id headers. See Authentication.

Concepts

The flow end to end

1

Push the purchase order

POST /v1/purchase-orders with the supplier and its lines. poNumber is unique per organisation. To re-ingest a changed PO idempotently, PUT /v1/purchase-orders/by-number/{poNumber}.
2

Push delivery notes as goods arrive

POST /v1/delivery-notes with the received lines. A supplier can send many partial deliveries against one PO — push each as its own note.
3

Dolfin builds the match group

When a bill for the same supplier enters review, Dolfin lazily creates a match group and aligns lines against both the POs and the DNs: exact sku matches auto-allocate, close description matches surface as suggestions to confirm, and anything else is allocated manually. PO, DN, and bill can arrive in any order.
4

Read the derived view

GET /v1/bills/{billId}/match-group (or GET /v1/match-groups/{id}) returns every bill line with its allocations, the three variance kinds, and fuzzy PO and DN suggestions.
5

Confirm suggestions and fix allocations

Confirm a suggestion or add a manual match with POST /v1/match-groups/{id}/allocations; unmatch with DELETE /v1/match-groups/{id}/allocations/{allocationId}.
6

Resolve variances

For each varianced line, POST /v1/match-groups/{id}/resolve records a human decision. Once every varianced line is accept-resolved the group settles.
7

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.

Push a purchase order

Re-uploading the same PO later? Address it by number so the ingest is idempotent — lines with a known id update, new lines are added, omitted lines are removed:

Push a delivery note

One note per delivery. The sku is what lets Dolfin auto-match the received line to the bill line; dnNumber is required.

Read the match group

The derived view is the heart of the integration. Per bill line it carries the billed figures, the PO-side numbers (allocatedNet, orderedQuantity, priceVarianceTotal), the DN-side numbers (receivedQuantity, quantityVariance), the three has…Variance flags, the backing allocations, and any recorded resolution. It also returns two suggestion lists — suggestions (fuzzy PO-line matches) and dnSuggestions (fuzzy DN-line matches) — so your UI can ask “which order?” and “which delivery?” without the caller knowing line ids.
Key fields on each line:

Allocate a bill line (or confirm a suggestion)

An allocation is the single primitive for both legs. Pin a PO line for the price leg, a DN line for the receipt leg, or both at once — at least one is required. Set method to Fuzzy and pass the suggestion’s confidence when you’re confirming a suggested match; method defaults to Manual. matchedAmount is optional — omit it and the service derives it from the bill line’s unit price.
To confirm a fuzzy PO suggestion, send its poLineId with "method": "Fuzzy" and the suggestion’s confidence; a DN suggestion is the same with dnLineId.
The double-allocation guard applies to both legs. The total matched against a single PO line can never exceed its ordered quantity (HTTP 409 Match.OverAllocation), and the total matched against a single DN line can never exceed its received quantity (HTTP 409 Match.DnOverReceipt). Both totals count every bill, so you cannot pay the same order — or the same delivery — twice.
Unmatch by deleting the allocation — the allocation → line foreign keys are Restrict, so this is the only unmatch path. The group’s status re-derives on the response:

Move a delivery line to a different bill

A delivery-note line’s received quantity is counted across every bill, whichever state those bills are in. So when a second bill legitimately covers a delivery that another bill already matched, your allocation is refused — even though that other bill is still in review. Two different situations refuse it, and your next step is the opposite in each: Match.DnLineHeld carries everything you need to release the hold, so you never have to search for it:
Delete the named allocation, then re-post yours:
A match can only be changed while the bill holding it is in PendingReview. Once that bill is approved its allocations are frozen and the delete returns 409 Match.BillNotInReview — handle that as “this line can’t be reassigned”, not as a retryable failure.
Releasing a hold changes the other bill: it clears any resolution recorded on that line, re-derives that bill’s match status (which can emit match_group.variance) and holds it at the approval gate again. That is correct — it no longer covers the delivery — but it is visible to whoever is reviewing it, so surface it rather than doing it silently. To ask the question ahead of time rather than by hitting the error, GET /v1/delivery-notes/{id} reports matchedQuantity and fullyMatched per line, and GET /v1/delivery-notes/{id}/matched-bills lists the bills holding the note.

The three variances

All three are derived at read time and only flag when they exceed the organisation’s rounding tolerance:

Resolve a variance

Resolution is per bill line — put the billLineId in the body. One action settles that line’s variance whatever its kind:
Re-posting overrides a line’s prior decision. To undo, clear the resolution — idempotent, and the group’s status re-derives:
Once every varianced line is accept-resolved, the group settles and Dolfin emits the BillMatchResolved fact. Resolving does not auto-approve the bill: the bill stays in review, and the match gate re-evaluates the now-clean group when you re-submit it.

The approval gate

A bill cannot leave review while its match group is unresolved. When you POST /v1/bills/{id}/submit-review, the submit-time gate re-checks the group; a clean or fully-resolved group lets the bill proceed to approval, an unresolved one holds it. Matching produces the fact; your approval policy decides what to do with it.

Cross-referencing

From either side of the match you can list the bills it touches — this is also how you find which bill is holding a delivery line you want to reassign:
Both POs and DNs can be retired from future matching with .../close (done/fulfilled) or .../cancel (void). Either is terminal and immutable; existing allocations are kept.

Webhooks

Drive an event-based integration off match-group lifecycle changes instead of polling. See the webhooks guide for how to subscribe, verify signatures, and handle retries. Each payload carries a matchGroup summary — id, billId, and status only. Fetch GET /v1/match-groups/{id} for the full derived view with lines, allocations, and variances.

Notes

  • Allocate on net. Tax is computed separately, so per-line tax rounding never shows up as a fake price variance. The PO allocation owns the net; a DN-only allocation is receipt-only.
  • SKUs drive automation. Exact sku matches auto-allocate (method: Exact). Without SKUs, every line falls to fuzzy suggestions or manual allocation — populate sku on PO and DN lines wherever you can.
  • Tolerance is per organisation. The rounding tolerance (default 1p) feeds both line reconciliation and the clean-match threshold, so a difference within it is treated as clean.