An invoice extraction score is evidence about a field, not authority to create an accounting state. Build three-way matching as a deterministic gate around the model: join the invoice, purchase order and receipt under a versioned policy, return draft, review or hold, then read the accounting record back before the workflow advances.
The model's job is to propose structured invoice evidence. The application's job is to decide whether that evidence may change a business record. Mixing those jobs makes a confidence threshold behave like an approval policy and turns a clean extraction into a potentially unsafe accounting action.
A bounded gate keeps the distinction explicit. It accepts one normalized invoice, one authoritative purchase order, one receipt and one policy version. It emits a typed outcome with the evidence an operator needs to reproduce the decision. Payment remains outside the gate.
The dependency-free reference artifact behind this article passes 15 of 15 local test cases. That proves its branches behave as written. It does not prove that its sample policy is correct for a buyer's accounting system.
Confidence cannot approve an invoice
Use extraction confidence to decide when evidence needs review, never to decide whether an invoice is payable.
Microsoft's prebuilt invoice model can return invoice ID, vendor, purchase-order reference, totals, dates, payment terms and line items. It also returns confidence for those fields and documents a flow that sends low-confidence output to a custom model. Microsoft Learn on invoice processing.
That confidence answers a narrow question: how strongly did the extractor recognize this field? It does not establish that the purchase order is open, the receipt belongs to that order, the supplier and currency agree, the variance is within the buyer's policy, or the invoice is not a duplicate.
The safe composition is:
- The model extracts values and confidence.
- The application normalizes identifiers and amounts.
- The gate joins authoritative order and receipt evidence.
- A versioned policy returns draft, review or hold.
- A permitted draft write is read back before the workflow advances.
The buyer owns the threshold. The reference code uses 0.90 only as an illustrative policy setting. Microsoft's documentation uses 0.65 in one low-confidence fallback example, not as a universal approval threshold.
Define three outcomes before the model runs

Return outcomes that describe the next safe operation, not a generic matched: true flag.
Draft means the required evidence satisfies the current policy and the application may propose one draft accounting record. It does not mean approved or paid.
Review means evidence is missing, low-confidence or outside a tolerance that requires a person to decide. The outcome names the missing or conflicting fields and the review queue.
Hold means an explicit boundary applies: duplicate business key, supplier or currency mismatch, ineligible purchase-order state, receipt from another order, or an attempted payment action outside the pilot.
The type makes authority visible:
type MatchOutcome = "draft" | "review" | "hold";
type MatchDecision = {
kind: MatchOutcome;
reason: string;
businessKey: string;
evidence: {
invoiceSourceId: string;
purchaseOrderId: string | null;
receiptId: string | null;
policyVersion: string;
intendedState?: DraftInvoiceState;
[key: string]: unknown;
};
};Keep the reason enumerable in production even if the reference artifact uses readable strings. Stable reason codes make alerting, sampling and policy analysis possible without parsing prose.
Put matching policy in deterministic code
Three-way matching is an evidence join. Oracle defines matching as associating an invoice with a purchase order, receipt or consumption advice. Its validation flow checks variances against configured tolerances and can place holds for exceptions before payment or accounting entries advance. Oracle on matching invoice lines and Oracle on invoice validation.
Keep that join outside the model. A compact gate can make each branch inspectable:
function decideThreeWayMatch(input: MatchInput): MatchDecision {
const key = invoiceBusinessKey(input.invoice);
if (input.requestedAction !== "draft") {
return hold(key, "requested action exceeds pilot authority");
}
if (input.seenBusinessKeys.has(key)) {
return hold(key, "duplicate business key");
}
if (!input.purchaseOrder) {
return review(key, "purchase order missing");
}
if (!input.policy.allowedPoStatuses.includes(input.purchaseOrder.status)) {
return hold(key, "purchase order state is not eligible");
}
if (normalize(input.invoice.supplierId) !== normalize(input.purchaseOrder.supplierId)) {
return hold(key, "supplier does not match purchase order");
}
if (normalize(input.invoice.currency) !== normalize(input.purchaseOrder.currency)) {
return hold(key, "currency does not match purchase order");
}
if (!input.receipt) {
return review(key, "receipt missing");
}
if (input.receipt.purchaseOrderId !== input.purchaseOrder.id) {
return hold(key, "receipt does not belong to purchase order");
}
const lowConfidence = requiredFields.filter(
(field) => input.invoice.confidence[field] < input.policy.confidenceFloor,
);
if (lowConfidence.length) {
return review(key, "required extraction evidence is below policy", {
lowConfidence,
});
}
if (outsideTolerance(input.invoice.totalMinor, input.purchaseOrder.totalMinor, input.policy)) {
return review(key, "invoice total exceeds purchase-order tolerance");
}
if (outsideTolerance(input.invoice.totalMinor, input.receipt.receivedTotalMinor, input.policy)) {
return review(key, "invoice total exceeds receipt tolerance");
}
return draft(key, "three-way evidence satisfies policy");
}Three implementation details carry most of the safety.
First, compare money in integer minor units. A $1,250.00 invoice becomes 125000, and a one-dollar illustrative tolerance becomes 100. The buyer must decide whether a tolerance applies by amount, percentage, line, supplier or document class. The reference code models one amount tolerance because it is an inspectable starting point, not because it is a complete AP policy.
Second, create a stable business key before any write. The reference key hashes normalized supplier ID, invoice number, currency and total. A production key may also bind legal entity or source namespace. It must remain stable across redelivery so an ambiguous retry cannot create a second draft.
Third, preserve the exact evidence used. Do not retain only the result. Store normalized invoice values, order and receipt identities, confidence, policy version, variance and terminal reason. The invoice-processing pilot worksheet uses that retained outcome as its cost denominator.
Read the accounting record back

Treat write intent, provider acceptance and authoritative observation as separate evidence.
After a draft decision, reserve the stable business key, write once, retain the provider response and read the record back by provider ID or external key. Compare only fields that define the business effect: status, supplier, invoice number, currency, total, purchase-order association and receipt association.
function reconcileDraft(decision: DraftDecision, observed?: DraftInvoiceState) {
if (!observed) {
return { kind: "uncertain", reason: "authoritative read-back unavailable" };
}
const differences = authoritativeFields.filter(
(field) => normalize(decision.evidence.intendedState[field])
!== normalize(observed[field]),
);
if (differences.length) {
return { kind: "divergent", differences, observed };
}
return { kind: "observed", recordId: observed.recordId, state: observed };
}An unavailable read-back is uncertain. A record that exists with the wrong total or association is divergent. Neither state permits a blind replay. The broader tool receipt and outcome verification contract explains why an accepted call is not yet a completed business effect.
The one control that matters most here is observation. Confidence can improve extraction. Deterministic matching can improve the proposal. Duplicate reservation can elect one writer. Only read-back establishes whether the authoritative accounting state retained the intended effect.
Test ambiguity and authority, not only exact matches
The reference artifact passes 15 local cases with Node's built-in test runner. They cover five distinct failure classes:
- Evidence completeness: missing purchase order, missing receipt and low-confidence required fields route to review.
- Policy variance: purchase-order and receipt totals outside the illustrative tolerance route to review; a value inside it may draft.
- Identity integrity: supplier mismatch, currency mismatch, wrong receipt association and duplicate business key hold the workflow.
- Authority: a payment request and a closed purchase order hold rather than silently narrowing into a draft.
- Remote ambiguity: matching read-back becomes observed, missing read-back remains uncertain and contradictory read-back becomes divergent.
Add buyer-specific cases before release: partial receipts, credit memos, freight, tax, multi-currency conversion, line-level tolerances, service entry, split purchase orders and duplicate numbering across suppliers. Each new document class or action is a new policy version and release candidate.
Run the gate against frozen representative cases, then against the real provider sandbox or test tenant. Provider schemas, state transitions, uniqueness and eventual consistency are part of the runtime. A pure function test proves the rule branches. It cannot prove the adapter or accounting system applies them correctly.
Release the narrow state change first
Start with draft and hold state only. Leave approval and payment in the buyer's existing process until the workflow can produce complete evidence under representative traffic.
The release record should bind:
- invoice source and document classes,
- legal entity and authoritative accounting system,
- matching policy version and owners,
- allowed reads and the exact draft or hold write,
- duplicate key and reservation store,
- review queue and acknowledgement rule,
- read-back fields and observation window,
- rollback to manual intake,
- evaluation cases and accepted outcomes,
- cost window and unresolved records.
Do not expand from draft creation to approval, payment or vendor-master mutation because the first window looks clean. Those actions change the loss boundary and recovery path. Qualify each one as a separate release.
What documents are needed for three-way invoice matching?
Use the supplier invoice, purchase order and goods receipt or service-entry evidence. The authoritative system also needs stable identities that let the workflow prove those records belong to the same business event.
What is the difference between two-way and three-way invoice matching?
Two-way matching compares the invoice with the purchase order. Three-way matching adds receiving evidence, so the workflow checks what was billed against what was ordered and what was recorded as received.
What are common three-way matching errors?
Common failures include missing receipts, supplier or currency mismatch, duplicate invoices, amount or quantity variance, a receipt linked to another order and low-confidence extraction on a required field.
How should invoice discrepancies be resolved?
Return review or hold with the exact conflicting evidence and a named owner. Correct the source record or obtain the missing evidence, then evaluate again under a recorded policy version. Do not overwrite the discrepancy or replay an ambiguous write.








