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

# Three-way Matching

> Match purchase orders to bills, surface variances, and gate approval

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.

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

## Concepts

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

## Lifecycle

<Steps>
  <Step title="Raise a purchase order">
    `POST /purchase-orders` with the supplier and its lines.
  </Step>

  <Step title="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.
  </Step>

  <Step title="A variance surfaces">
    A price variance outside the organisation's rounding tolerance moves the group to `Variance`.
  </Step>

  <Step title="A reviewer resolves it">
    `accept_price` (or `dispute`, …) settles the match. A clean or accepted match becomes
    `Resolved`.
  </Step>

  <Step title="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.
  </Step>
</Steps>

## Create a purchase order

```bash theme={null}
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`.

```bash theme={null}
# 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)

```bash theme={null}
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" }'
```

<Warning>
  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.
</Warning>

## Resolve

```bash theme={null}
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:

| Event                  | When                                  |
| ---------------------- | ------------------------------------- |
| `match_group.created`  | a match group is created for a bill   |
| `match_group.variance` | a variance needs review               |
| `match_group.resolved` | the 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.
