Three-way match automation: tables and workflow for AP holds
Learn three-way match automation with practical table designs and a visual workflow that holds payments until PO, receipt, and invoice quantities and prices match.

What problem three-way matching actually solves
Three-way match automation is simple: you pay an invoice only when it matches what you ordered and what you actually received. The three documents are the purchase order (PO), the receiving record (receipt), and the supplier invoice.
Without this check, accounts payable can end up paying based on a single document that is wrong or incomplete. A supplier might bill for more units than were delivered, use a different price than agreed, or send a duplicate invoice that looks new in an email thread.
These failures rarely look dramatic on day one. They show up as small leaks: a line item billed twice, a shipment short by a few units, a price increase that was never approved, or freight added when it shouldn’t be. Over time, those small mistakes turn into real money.
The point isn’t to “approve invoices.” The point is to block payment until the key fields you choose (usually quantity, unit price, and totals) line up across the PO, receipt, and invoice. When they don’t match, the invoice shouldn’t vanish into email. It should land in an exception queue with a clear reason code and the exact fields that differ.
Three-way match also draws a clean line between teams. Procurement owns what was ordered (terms and price). Receiving confirms what arrived (quantities and dates). Finance controls what gets paid (invoice review and release).
Set expectations early: this is a process plus data problem, not an approval button. If PO lines are vague, receipts aren’t recorded, or invoices can’t be tied to a PO line, automation won’t save you.
Documents and roles: PO, receipt, invoice, and who owns what
Three-way match works only when each document has a clear owner. If “who updates what” is fuzzy, the system either blocks good payments or lets bad ones through.
A practical ownership model is:
- Requester creates the purchase request and confirms the need.
- Procurement creates and maintains the PO (supplier, price, terms).
- Warehouse/receiver (or the service owner) posts the receipt or acceptance.
- AP/Finance records the invoice and controls payment.
Each document also needs a minimum set of fields so matching isn’t guesswork.
PO (purchase order) needs supplier ID, PO number, line items (SKU or service), ordered quantity, unit price, currency, tax rules, and payment terms.
Receipt needs a PO reference, receipt date, received quantities by PO line, and who received it. For services, treat it as acceptance and record the approver.
Invoice needs supplier invoice number, invoice date, PO reference (or a safe way to find the PO), line details (qty, unit price), taxes/shipping, and the total.
Also decide when the match runs. It shouldn’t be a one-time event. Trigger it whenever reality changes:
- When an invoice is captured (so you decide pay vs hold immediately).
- When a receipt is posted (so an invoice on hold can become payable).
- When a PO is changed (so open invoices are re-checked).
Partial receipts and multiple invoices are normal. A PO line might arrive in three deliveries and be billed across two invoices. Your logic should compare cumulative received vs cumulative invoiced per PO line, not just one document at a time.
Rules to decide before you build anything
Before you touch tables or workflow steps, agree on the rules that will drive the whole system. Vague rules create predictable failure: either the system blocks too much (so people bypass it), or it blocks too little (so bad invoices still get paid).
Pick the matching level. Header-only matching checks totals at the document level. It sounds easier, but it breaks fast with partial deliveries, backorders, freight lines, or mixed tax rates. Line-level matching takes longer to set up, but it’s the safer default because you compare the same thing across PO, receipt, and invoice: the specific line, its quantity, and its unit price.
Define hard blocks vs warnings. A hard block means payment can’t proceed until the issue is resolved. A warning means the invoice can move forward, but someone must acknowledge the risk.
Typical starting points:
- Hard block: quantity invoiced exceeds quantity received (for goods).
- Hard block: unit price exceeds PO price beyond tolerance.
- Warning: small rounding differences.
- Warning: tax or shipping differences that are expected and coded separately.
Keep tolerance rules explicit. Define the method (percent, absolute amount, or the higher of the two) and who owns it. Example: allow +/- 1% or +/- $5 per line, with finance allowed to change tolerances only with an audit note.
Use a small, shared status set. Avoid custom statuses per team. A clean set is usually enough: Matched, Hold, Exception, Approved. “Hold” means payment is blocked. “Exception” means a human needs to review it. “Approved” means a named person accepted the mismatch and recorded why.
Data model: the tables you need (and why)
Three-way match automation works only if your data model can line up a PO line, what was received, and what was invoiced. Every invoice line should be matchable to a specific PO line (or clearly marked as non-PO), and every receipt line should reduce the remaining quantity on that PO line.
Start with core purchasing tables:
- Vendors: one row per supplier (name, terms, tax info).
- ItemsServices: optional, but helps consistency (SKU, description, unit of measure).
- PurchaseOrders: PO header (vendor_id, currency, requested_by, status).
- PO_Lines: the matching anchor (po_id, item_id/description, ordered_qty, unit_price).
Receiving needs its own records, even if a “receipt” is just a confirmation. Keep receipts separate from invoices so you can prove what arrived and when:
- Receipts: receipt header (vendor_id, received_date, location, status).
- Receipt_Lines: each line references the PO line (receipt_id, po_line_id, received_qty, notes).
Invoicing mirrors receiving. Store what the supplier billed at line level and connect it to the PO line that should cover it:
- Invoices: invoice header (vendor_id, invoice_number, invoice_date, due_date, status).
- Invoice_Lines: (invoice_id, po_line_id when applicable, invoiced_qty, unit_price, tax, line_total).
Finally, create a payment-facing record your workflow can block. Some teams call this a bill, payment request, or pay run item:
- PaymentRequests (or Bills): ties to invoice_id and includes payment_hold (true/false) plus hold_reason.
For audits and clean exception handling, add consistent lifecycle fields on headers (POs, receipts, invoices, payments): status, created_at/created_by, approved_at/approved_by, posted_at, and (optionally) source_document_id for imports.
Key fields and relationships that make matching reliable
Matching works best when every document traces back to the same line item. That means stable IDs, clean links, and totals that can be recalculated from the lines.
Make sure each table has both a stable internal ID and the external number people search for:
- PO header: po_id, po_number, vendor_id, currency, status, po_date
- PO lines: po_line_id, po_id, item_id or description, ordered_qty, unit_price, tax_rate, line_total
- Receipts: receipt_id, receipt_number, vendor_id, received_date; receipt_line_id, receipt_id, po_line_id, received_qty
- Invoices: invoice_id, vendor_id, vendor_invoice_number, invoice_date, currency, subtotal, tax_total, total; invoice_line_id, invoice_id, po_line_id, qty, unit_price, tax_amount, line_total
- Vendors and items: vendor_id, payment_terms, default_currency; item_id, uom, tax_code
The most important links are line-level:
- invoice_line.po_line_id should point to the PO line.
- receipt_line.po_line_id should point to the same PO line.
This is what lets you compare quantity and price without guessing.
To handle partials, calculate running totals per PO line: received_qty (sum of receipt lines) and invoiced_qty (sum of invoice lines). Then compute remaining_qty = ordered_qty - received_qty and open_to_invoice_qty = received_qty - invoiced_qty. These values make it obvious whether an invoice is early, late, or overbilling.
Don’t overwrite history when a PO changes. Store a PO revision number and either keep old PO lines (with an active flag) or write a change log (who changed what, when, old value, new value).
Add basic guardrails to prevent duplicates and bad joins:
- Unique (vendor_id, vendor_invoice_number)
- Unique receipt_number and po_number
- Not null on currency, quantities, and unit_price
- Check constraints like qty >= 0 and unit_price >= 0
- Foreign keys from invoice_line and receipt_line to po_line
Step-by-step workflow: from invoice intake to payment hold
Three-way match automation usually has three entry points: an invoice arrives (email, upload, EDI), a receipt is posted, or a PO is changed (price, quantity, status). The workflow should react to any of these so an invoice can move off hold as soon as the missing piece shows up.
1) Validate the invoice basics first. Confirm the vendor is active, the PO exists, currency matches the PO, and totals are internally consistent (line totals add up, tax is reasonable, no negative quantities unless you support credits). If this fails, send the invoice straight to Hold with a clear reason.
2) Match per line, not just at the header. For each invoice line, find the related PO line and receipt totals to date. Compare:
- Quantity invoiced vs quantity received (or received minus already invoiced)
- Unit price invoiced vs unit price on the PO
- Tolerance rules
- Whether the PO line is still open for invoicing
3) Set status and enforce blocking. A common pattern:
- Matched: all lines pass checks, no open exceptions.
- Hold: at least one line fails, or required data is missing.
When Hold is set, create a payment hold record that the payment run must honor. Keep holds separate from the invoice so holds can be added, released, or replaced without rewriting invoice history.
4) Record reason codes finance can trust. Avoid free-text-only holds. Use codes like PRICE_OVER_TOLERANCE, QTY_NOT_RECEIVED, PO_CLOSED, VENDOR_MISMATCH, or CURRENCY_MISMATCH, plus a short note.
Exception queue design for finance (what to store and what to show)
An exception queue is where matching becomes usable, not just strict. Finance should see only invoices that need a decision, with enough context to decide quickly and leave a clean audit trail.
A common approach is a dedicated table like ExceptionCases. Each row represents one blocked invoice (or invoice line) and points back to the invoice, PO, and receipt records. Keep the matching engine read-only here. The queue is for decisions and documentation.
What to store in ExceptionCases
Store what’s wrong, how big it is, who owns it, and what happens next:
- Type (missing receipt, price variance, quantity variance, PO not found, duplicate invoice)
- Severity (info, warning, block) plus a user-friendly reason
- Owner (person or team) and status (open, waiting on vendor, waiting on warehouse, resolved, overridden)
- Variance snapshot as sortable numbers (invoice amount, matched amount, price delta, quantity delta)
- SLA fields (due date, escalation flag, reassigned_at, reassignment_reason)
Also store collaboration and audit data: comments (author, timestamp) and attachment metadata (file name, type, uploaded_by, uploaded_at). Even if files live elsewhere, the metadata belongs in the case so the history stays intact.
What finance should see (and do)
The queue view should be a tight worklist: vendor, invoice number, exception type, severity, amount, due date, owner, and a clear “why blocked” message.
Opening a case should show a side-by-side summary: PO lines, receipt quantities, invoice lines, and the exact fields that failed.
Keep actions limited and safe:
- Request receipt (routes to receiving, sets status to waiting)
- Request credit memo (routes to vendor, records expected adjustment)
- Approve override (requires reason, captures approver and timestamp)
- Reassign (updates owner, keeps reassignment history)
- Close as resolved (only after changes make the match pass)
Example: an invoice is blocked because 8 units were received but 10 were billed. Finance can request a corrected invoice, or approve an override for the 8 received units and keep the remaining 2 on hold.
Realistic example: partial receipt and a mismatched invoice
A buyer creates a PO for 100 units of item A at $10.00 each. The PO total is $1,000. Two days later, the warehouse posts a receipt for 80 units.
Then an invoice arrives for 100 units at $10.00 each. The match should compare the invoice lines to what has been received, not just what was ordered.
On that line:
- Ordered: 100 units
- Received: 80 units
- Invoiced: 100 units
- Matched quantity: min(Received, Invoiced) = 80 units
- Unmatched quantity: Invoiced - Matched = 20 units
The invoice becomes “On hold” because 20 units have no receipt. Finance sees a case with a clear reason (Quantity variance: +20) and the key numbers side by side.
Notifications should go to whoever can fix it fastest: usually the receiver (to confirm whether a receipt is missing) and the buyer (to follow up if the shipment is actually short).
When the remaining 20 units arrive, the warehouse posts a second receipt for 20 units. The system re-runs matching: received becomes 100, unmatched becomes 0, the invoice moves to Matched, and the hold is released.
Now add a price variance. If the supplier invoices 100 units at $10.50 instead of $10.00, quantity matches but price doesn’t. Expected outcome: keep the invoice on hold and route it with a reason like “Price variance: +$0.50/unit (+$50 total).”
Common mistakes that break three-way match workflows
Most matching failures aren’t about math. They come from weak data links and loose controls over posted documents.
Matching only at the invoice total. A header can look fine while one line is overpriced or short. Do line-level matching, and be explicit about what can differ (often freight) and what can’t (received quantity and unit price).
Assuming one receipt and one invoice per PO. Real purchasing includes split shipments and partial billing. Support many receipts and many invoices against the same PO lines, and track remaining open quantity per line.
Allowing edits to posted receipts or invoices without a trail. If someone can quietly change quantities later, the match stops being evidence. Lock posted records and correct through adjustment documents that preserve history.
Missing duplicate prevention. The same vendor invoice number can be entered twice, or a PDF can be uploaded again by another person. Add uniqueness early (vendor + invoice number, and optionally date/amount) and show a clear message when a duplicate is detected.
Vague exception reasons. Finance shouldn’t have to guess. Use reason codes that route cleanly: price mismatch, quantity mismatch, missing receipt, duplicate suspected, PO not found, vendor mismatch.
Quick checklist before you turn on payment blocking
Payment blocking is where matching stops being a report and becomes a control. If the basics aren’t solid, you’ll create noise for finance and late payments for vendors.
Test a small set of invoices that look different: a clean match, a partial receipt, a price change, and a tax difference. If any can’t be matched cleanly, fix the data and rules first.
Checklist:
- Reference completeness: every invoice has a vendor and a PO reference, and each invoice line can map to a specific PO line (not just “PO total”). Decide what happens when a vendor sends only a PO header number.
- Consistent math: quantities, unit prices, and totals recalculate the same way every time. Be explicit about tax, freight, discounts, and rounding (for example, rounding per line vs only at the invoice total).
- Statuses block early enough: set “on hold” before any payment request or payout record is created.
- Structured exceptions: every hold stores a reason code and an owner (AP, buyer, receiver). Add due dates so holds don’t sit forever.
- Real audit trail: overrides record who approved, when, and what they approved (including original values). If you allow edits, log before and after values.
Next steps: pilot the process and build it visually
Treat three-way match automation like any control: prove it works on a small slice of spend, then roll it out.
Start with a pilot that’s easy to monitor. Pick one business unit, a small vendor group that sends clean invoices, or a single item category. Keep rules strict at first (exact quantity and price match) so data quality issues show up quickly.
Measure success with a simple finance view: holds per week, top reason codes, time from hold to release, how many holds were true mismatches, and which vendors create repeat exceptions.
If you want to prototype quickly, a no-code platform can help because you can model tables, matching rules, and routing without writing code. For example, in AppMaster (appmaster.io) you can build the PO, receipt, invoice, and exception tables, then connect the hold logic in a visual business process so the same rules run on every trigger.
Test with real invoices from the pilot group, including partial receipts and common vendor mistakes. Expect to tweak matching keys and add small tolerances only after you see patterns. Once holds look reasonable and resolution times improve, expand scope and add richer rules (tax and freight handling, unit-of-measure conversions, split shipments) without losing the core control: no payment release until the match is cleared.


