Rivane

Accounting
made smart

Procure to pay control design

Published April 22, 2026

Three-way match is not an AP task. It is the control gate between procurement, receiving, AP, and cash.

A practical design for matching purchase orders, goods receipts, and supplier invoices without hiding exceptions, weakening audit evidence, or letting duplicate payments slip through.

The operating thesis.

A weak three-way match workflow makes AP look fast while moving risk into GRNI, supplier disputes, and payment runs. A strong workflow does the opposite: it makes clean invoices almost invisible and makes every exception obvious, owned, timed, and explainable.

The design below treats three-way match as a stateful ERP control. It separates commercial agreement, physical receipt, invoice assertion, accounting posting, exception ownership, and payment readiness. That separation is what makes the workflow useful to CFOs, controllers, AP managers, procurement teams, warehouse teams, and auditors.

For US and UK operators, the practical bar is simple: a matched invoice should explain itself later. The ERP should show the source purchase order, receiving evidence, invoice payload, tolerance policy, journal impact, approval history, exception trail, payment hold status, and every override.

Diagrams that define the workflow.

Diagram 1

Operating workflow

The happy path is intentionally narrow. Anything outside tolerance leaves the auto-match lane and becomes a buyer-owned exception.

1

Supplier invoice received

2

Normalize invoice lines

3

Load PO commitments

4

Load goods receipt lines

5

Apply tolerance policy

6

Post GRNI and AP

7

Queue payment

Diagram 2

Invoice line state machine

A line, not only an invoice header, needs a lifecycle. Partial receipts, rejected variances, and approved overrides must not collapse into a vague pending status.

PENDING_MATCH
AUTO_MATCHED
PAYMENT_QUEUED
EXCEPTION
BUYER_REVIEW
RESOLVED
REJECTED
OPEN_RECEIPT
FromToGuard
PENDING_MATCHAUTO_MATCHEDPO, GRN, invoice line inside tolerance
AUTO_MATCHEDPAYMENT_QUEUEDGRNI and AP posting committed
PENDING_MATCHEXCEPTIONPrice, quantity, tax, supplier, or currency mismatch
EXCEPTIONBUYER_REVIEWOwner assigned and SLA clock started
BUYER_REVIEWRESOLVEDCorrected document or approved variance
BUYER_REVIEWREJECTEDInvalid invoice, duplicate, blocked supplier, or failed policy
PENDING_MATCHOPEN_RECEIPTInvoice arrived before full receipt
OPEN_RECEIPTPENDING_MATCHAdditional receipt posted

Diagram 3

Cross-team sequence

The durable workflow shows which system or team owns each decision. AP should not silently resolve commercial differences that belong to procurement.

Supplier
AP
ERP
Warehouse
Buyer
Ledger
1
Supplier
Submit invoice
AP
2
AP
Create match run
ERP
3
ERP
Read receipt evidence
Warehouse
4
ERP
Route exception if variance exceeds policy
Buyer
5
Buyer
Approve variance or dispute invoice
ERP
6
ERP
Post GRNI debit and AP credit
Ledger
7
ERP
Expose payment-ready status
AP

Diagram 4

Data model boundary

The match record is the join table for commercial agreement, receipt evidence, supplier assertion, exception handling, and ledger impact.

Diagram 5

Accounting impact

The matched line should clear GRNI and create the AP liability in the same commit. A split commit is how reconciliation debt is born.

Controls, exceptions, and evidence.

Control matrix

ControlFailure it preventsEvidence to preserve
Line-level PO, GRN, and invoice joinHeader-level approval hiding line varianceMatched line id, PO line id, GRN line id, invoice line id
Independent price and quantity toleranceA quantity overage being disguised as a price varianceTolerance policy version and computed variance
Buyer-owned exception queueAP approving commercial changes without contextOwner, SLA, buyer decision, reason code
Atomic GRNI and AP postingGRNI cleared without a corresponding liabilityJournal id, transaction id, debit line, credit line
Idempotent match runDuplicate match run from retry or integration replayExternal id and original match run id

Exception taxonomy

ExceptionLikely ownerResolution path
Invoice before receiptWarehouseHold line in OPEN_RECEIPT until receiving evidence exists
Price varianceBuyerApprove variance, request credit memo, or ask supplier to reissue
Quantity varianceWarehouse and buyerCorrect receipt, split invoice, or dispute excess quantity
Duplicate invoiceAPReject and link to existing invoice or payment
Blocked supplier or sanctions hitProcurement and complianceHold payment until supplier status is cleared

Audit evidence checklist

EvidenceWhy it matters
Original invoice file or EDI payloadShows what the supplier asserted
PO approval chainShows commercial agreement before the invoice arrived
Goods receipt timestamp and receiverShows physical receipt or service confirmation
Tolerance policy versionShows which rule allowed or blocked auto-match
Exception assignment and commentsShows ownership and reasoned resolution
Journal posting transaction idConnects operational match to ledger impact

API, error, and database contracts.

Match creation payload

{
  "external_id": "match_run_2026_07_01_supplier_5821_invoice_88401",
  "invoice_id": "inv_88401",
  "purchase_order_id": "po_43822",
  "goods_receipt_id": "grn_99140",
  "policy": {
    "price_tolerance_basis_points": 200,
    "quantity_tolerance_units": 0,
    "tax_tolerance_minor": 100
  },
  "requested_by": "ap_clerk_17"
}

Exception response for a price variance

{
  "type": "https://rivane.ai/problems/invoice-match.price-variance",
  "title": "Invoice line price exceeds tolerance",
  "status": 422,
  "detail": "Supplier invoice line 3 is 4.8 percent above the purchase order unit price.",
  "instance": "/v1/invoice-matches/match_129/exceptions/exc_44",
  "code": "invoice_match.price_variance",
  "owner_role": "buyer",
  "sla_deadline": "2026-07-03T17:00:00Z"
}

Relational constraints that prevent silent drift

alter table invoice_match
  add constraint invoice_match_status_check
  check (status in (
    'PENDING_MATCH',
    'AUTO_MATCHED',
    'PAYMENT_QUEUED',
    'EXCEPTION',
    'BUYER_REVIEW',
    'RESOLVED',
    'REJECTED',
    'OPEN_RECEIPT'
  ));

create unique index invoice_match_external_id_unique
  on invoice_match(entity_id, external_id)
  where external_id is not null;

create unique index invoice_match_line_once
  on invoice_match(invoice_line_id)
  where status in ('AUTO_MATCHED', 'PAYMENT_QUEUED', 'RESOLVED');

Implementation quality bar.

Implementation quality bar

AreaMinimum acceptable behavior
User experienceAP sees one queue with clear reason codes, not generic pending work
PerformanceBatch match runs are resumable and idempotent across retries
ControlsOverride permissions are separate from invoice entry permissions
ReportingGRNI aging reconciles to open receipt and open invoice lines
RecoveryA failed posting leaves no half-cleared GRNI state

Do not treat tolerance as a global setting. Supplier risk, item category, jurisdiction, currency, and receiving pattern all change the right tolerance design.

Do not let AP override procurement terms by editing invoice lines after match. Corrections should preserve the original assertion and create an explicit adjustment or supplier reissue path.

Do not collapse partial receipts into exceptions that look like errors. Partial fulfillment is normal. The state model should show that the line is waiting for receipt evidence.

Do not release payment only because an invoice header is approved. Payment readiness should come from line-level match status, supplier status, payment terms, tax handling, and hold checks.

Sources and reference context.

Book a Demo.

If your current three-way match workflow cannot show line-level state, exception ownership, GRNI impact, and payment readiness from one audit trail, the control is probably weaker than it looks.

Back to blog