Skip to content

Three-way matching for fintech teams

Your accounts payable (AP) system just approved a $48,000 invoice for 50 authorized units, but the units never arrived. The purchase order (PO) authorized 50 units; the vendor was billed for 50, but the receiving department logged 45. Because the invoice matched the purchase order, the system checked only the purchase order; nobody caught the mistake.

The $48,000 scenario is the failure mode that three-way matching is designed to prevent. Three-way matching is a payment control for fintech engineering teams that need to enforce accounts payable approvals in code. It matters because an invoice should not trigger money movement until the purchase order, receiving record, and vendor invoice agree.

Fintech engineering teams pay infrastructure suppliers and other vendors like anyone else, but they also have to build the systems that enforce the control programmatically.

What is three-way matching?

Three-way matching cross-checks three documents: the purchase order, the goods receipt note, and the vendor invoices, and then releases payment only when they agree on quantity, unit price, total amount, and PO number within configured tolerances.

  • Purchase Order (PO): generated by your purchasing team. Lists items, quantities, agreed unit prices, vendor details, and the PO number. The PO sets the commercial terms.
  • Goods Receipt Note (GRN): generated at receiving. Records delivery date, quantities received, and item condition. Without a GRN, you cannot prove anything arrived.
  • Vendor Invoice: generated by the supplier. Lists items, quantities, unit prices, and the total due. The invoice triggers matching in accounts payable.

Because these three records originate in three different systems, the hard part is getting the three views to agree on the same order.

However, three-way matching is more effective in specific cases than other matching formulas.

Two-way vs three-way vs four-way matching

Matching comes in three formulas, differing in which documents the control compares. There are two-way (PO + invoice), three-way (PO + GRN + invoice), and four-way (PO + GRN + inspection report + invoice) matching. Engineering teams should model the matching level as a configurable PO-level policy because each level adds a document and an additional verification criterion.

Matching typeDocumentsCommon use cases
Two-wayPO + InvoiceServices, digital goods, utilities, recurring low-risk spend, or established trusted suppliers where no physical delivery confirmation is needed
Three-wayPO + GRN + InvoicePhysical goods, inventory, capital purchases, orders with multiple shipments or high return rates
Four-wayPO + GRN + Inspection Report + InvoiceHigh-value, quality-critical, or regulated lines: pharmaceuticals, aerospace, medical devices, food manufacturing, or first-time suppliers

In many systems, two-way is the default; three- or four-way must be set explicitly on each PO at creation time.

Why fintech engineering teams care about three-way matching

Three-way matching prevents the payment system from approving invoices for goods or services never received, for quantities and prices that do not match, and to catch duplicate and fraudulent invoices that resemble a legitimate PO closely enough to pass. Most significantly, without three-way matching, fintech engineering teams are exposed to fraud.

In occupational fraud cases, asset misappropriation, which includes billing fraud, false invoicing, and payment tampering, occurred in 89% of cases, with a median loss of $120,000.

Vendor fraud can remain hidden for long periods, with the average discovery time being 18 months. Duplicate payments also contribute to leakage, with the industry estimating that they account for around 0.8% to 2% of total payments.

There is a regulatory dimension too. For organizations subject to audit requirements, the control must be documented, testable, and tied to the payment decision. GAO improper-payment guidance defines overpayments to include "payments for goods or services not received" and "any duplicate payment." The three-way matching system you build is also the system that your auditor will test.

How three-way matching maps into your systems

Three-way matching maps across three system boundaries: procurement (PO), receiving (GRN), and accounts payable (invoice), with a matching service that reads all three.

Because the records originate outside AP, the split across purchasing, receiving, and AP is the root architectural problem. If the three do not share data cleanly, teams end up reconciling the same order from different local views.

Engineering teams should model each document with a schema that exposes the fields that it depends on. The records need shared identifiers and access patterns that let a matching service read the purchase order, receiving record, and invoice together.

Use the PO number as the primary cross-reference between invoice and receipt records and the purchase order, and keep the vendor ID consistent across the PO and the invoice. A production model should handle requisition-to-PO and PO-to-receipt fan-out, including partial deliveries and multiple downstream documents, and maintain a queryable cross-reference between goods receipts and invoice receipts so the three-way relationship remains auditable.

This is the same architectural premise behind Formance Connectivity. It’s a single canonical schema downstream services can rely on, so a matching engine reads one source of truth instead of reconciling three drifted copies.

The four components to make three-way matching operational

Engineering teams must build four components to make three-way matching work: a header-plus-line-item data model for each document, a matching service that compares the three documents and applies tolerances, a payment workflow that gates on the match result, and an immutable ledger to record all approved decisions.

1. The data model for each document

Use a header plus line-item split for each document type. The header holds entity-level metadata (document number, vendor, dates, and totals); line items hold per-product detail (item code, quantity, unit price) so you can compare line by line. Account for fan-out: a single PO can contain many line items, and a shipment or invoice can map to multiple purchasing records.

2. The matching service to compare the three documents and apply tolerances

Use a job or API that pulls the three documents, compares line items at the header and line level, applies tolerances (such as unit price variance, quantity variance, total amount variance, freight, and tax), and creates exceptions for anything outside them.

3. The payment workflow to match results

Use a payment service that refuses to pay unless the three-way match is true or an exception has been explicitly approved. The match result gates payment. Treat the match outcome as a hard precondition on the payout call so that no invoice can be released to a bank, PSP, or payout rail without a passing match or a recorded override.

4. The immutable ledger to record all payments

Once a match passes and an invoice is approved, post the payment decision to an immutable, double-entry record so every dollar paid traces back to the match that authorized it. The historical record cannot be altered after the fact. At Formance, we build the open-source, programmable core ledger that unifies fiat and digital assets with regulatory-grade traceability, making it ideal for three-way matching.

Five stages of a three-way matching workflow

The end-to-end three-way matching workflow runs through five stages, including ingesting the PO, GRN, and invoice, matching items and quantities, applying tolerances, flagging mismatches as exceptions, and allowing or blocking payment.

1. Ingesting the PO, GRN, and invoice

Design ingestion adapters for procurement, receiving, and invoice channels such as API syncs and file transfers. Normalize each document into the shared schema before the matching service reads it, so downstream comparison is not held hostage to provider-specific formats.

2. Matching items and quantities

Run comparison at the header level (document numbers and vendor ID) and the line level (item codes, descriptions, quantities, and unit price). When a PO has multiple line items, evaluate the match on a per-line basis. Partial deliveries and split invoices are the norm, not the exception, so line-level granularity is what makes the control work in production.

3. Applying tolerances

Compare price, quantity, or total variance against configured thresholds. Resolve the most specific tolerance rule first (per supplier or category), then fall back to legal-entity defaults so the engine always has a deterministic threshold to evaluate against.

4. Flagging mismatches as exceptions

Anything outside tolerance becomes an exception with an actionable message. Tag the exception with the line number, the variance, and the rule that triggered it, so the resolver can act on it without re-running the comparison by hand.

5. Allowing or blocking payment

Clean matches route straight through. Failures get a hold until resolved or released. The match outcome is the gate on the payout call itself, so no invoice reaches a bank, PSP, or payout rail without a passing match or a recorded override.

Here is the line-level comparison the matching engine performs:

PO #PO-1234

Line 1: Widget A | Qty: 100 | $10.00/unit | Total: $1,000.00

Line 2: Widget B | Qty:  50 | $20.00/unit | Total: $1,000.00

GRN #GR-5678

Line 1: Widget A | Qty: 100 | Received

Line 2: Widget B | Qty:  45 | Received (partial)

Invoice #INV-9012

Line 1: Widget A | Qty: 100 | $1,000.00   match

Line 2: Widget B | Qty:  50 | $1,000.00   billed 50, received 45

Result: Exception: Quantity mismatch on Line 2

Line 2 is held because the invoice bills for 50 units against 45 received; Line 1 clears and posts.

When Line 1 clears, the corresponding payment posts to the ledger as a double-entry transaction in Numscript, Formance's language for financial transactions. The ledger model starts with the accounts:

Real-world partyLedger accountHolds
The platform's AP treasury@platform:treasury:apfunds available for approved AP payments
Acme, the vendor@counterparties:vendors:acme:payablewhat the platform owes the vendor

The approved invoice line moves $1,000.00 from the platform's AP treasury account to Acme's payable account.

// AP_INVOICE_APPROVAL
// Event: approved invoice line posts from AP treasury to vendor payable
send [USD/2 100000] (
  source = @platform:treasury:ap
  destination = @counterparties:vendors:acme:payable
)
set_tx_meta("event_type", "ap_invoice_approval")
set_tx_meta("invoice_id", "INV-9012")

The transaction commits atomically with the invoice number attached as metadata: either the full double-entry posting lands or nothing does.

Tolerances vs exceptions

Two configurations determine whether the matching engine clears a line or routes it for review: tolerance thresholds that define acceptable variance and exception routing that handles anything outside those thresholds. Tolerances set too tight choke on legitimate variances; set too loose, the control stops catching anything.

Tolerance thresholds that define acceptable variance

A 5% quantity and price per-line tolerance is one generous starting point for placing invoices on hold when variance exceeds configured limits. Many fintech AP systems run tighter than this, somewhere between 2–3% on price and zero tolerance on quantity is common.

Treat any starting figure as exactly that; your effective tolerance model should be configurable by supplier, category, line type, amount, and risk profile.

You should treat missing receipt evidence as non-negotiable. If GRN is null for a line, block payment regardless of price or quantity tolerance.

Four engineering patterns make tolerances maintainable:

  1. Tolerance rule entity: define each tolerance as a named rule with configurable upper and lower limits, including absolute and percentage fields where applicable.
  2. Scoping dimensions: scope each rule by legal entity (or equivalent organizational unit), with optional vendor_id, vendor_group, item_id, and item_group.
  3. Hierarchical resolution: evaluate the most-specific applicable rule first, then fall back through vendor, item, group, and legal-entity defaults.
  4. Separate templates per charge type: maintain distinct configs for unit price, quantity, totals, freight, tax, and cash discount.

Exceptions routing that handles anything outside thresholds

Exceptions determine what happens when a line is unclear. Define exception types such as price variance, quantity mismatch, missing GRN, tax calculation error, and shipping discrepancy, then route them based on exception details and department.

Keep the approval hierarchy configurable: dollar thresholds, exception types, departments, and approver roles should live in a configuration that finance can audit, and engineering can test.

The exception message must be actionable: "Line 3: Price variance of 5.2% exceeds tolerance of 3%" tells the resolver what to do; "Matching failed" does not. Route exceptions separately from matched invoices so that straight-through processing can continue and your STP rate remains measurable as a control health metric.

Automating three-way matching in a fintech stack

Automating three-way matching in a fintech stack spans three layers: integration, a centralized rules engine, and monitoring to surface control health. Engineering teams either build custom pipelines for each layer or reuse infrastructure.

Integration across procurement, receiving, AP, and payments

Connect procurement, receiving, AP, and payments using an API-first approach to reduce future breakage. Where you need immediate payment visibility, design syncs so that approved payment runs are reflected in the ledger without waiting for manual batch work. Disconnected systems force AP teams to re-enter data and manually match records.

Centralized rules engine that applies the right matching policy per invoice

Centralize matching rules and approval logic so that a single configurable engine applies the appropriate rule set for each vendor, category, and invoice type. The same platform should support 2-way, 3-way, and 4-way matching.

Monitoring that surfaces control health

Track operational metrics that show control health: AP aging, invoice exception rate, straight-through processing rate, unmatched invoice aging, and exception rate by type.

All three layers (integration, rules engine, and monitoring) produce one output that matters: a payment decision. That decision is only as trustworthy as the record it lands in, which is where the final piece of the architecture comes in.

Three-way matching is only as good as the ledger

Three-way matching is only executed properly when the ledger accurately reflects it.

A three-way match that posts to a mutable log can be quietly rewritten, and the control collapses. It stops payments your system should never make, such as for goods not received and invoices that fail agreed-price or prior-payment checks.

The matching is straightforward to reason about and demanding to build correctly, because the gate is only as trustworthy as the immutable, double-entry record behind it.

With Formance Ledger, you build an auditable record of every approved payment. At the same time, Formance's broader platform can help reduce reliance on brittle reconciliation scripts by normalizing provider data into a canonical schema.

_ledger

Run three-way matching on an immutable ledger

Explore and clone Formance Ledger on GitHub, so you can run it locally to back your three-way matching pipeline with an immutable, double-entry record of every approved AP payment.