Why a PO Number Isn't a Join Key: The Real Architecture Behind Invoice Matching

Sarbjeet Singh
Sarbjeet Singh

Lead-Front End Engineer

LinkedIn

Why a PO Number Isn't a Join Key: The Real Architecture Behind Invoice Matching

A data engineer at a mid-size manufacturer told us his matching logic worked perfectly in the demo. One PO number, one invoice, one clean SQL join. Match rate: 100%. Then the system went live against a real month of accounts payable data, and the match rate dropped to 61% by day three.

Nothing was wrong with the extraction. The PO numbers were correct. The invoice numbers were correct. What broke was the assumption underneath the entire design: that a shared identifier is enough to link two records that came from different systems, extracted at different times, describing a transaction that unfolds over weeks or months.

This happens on nearly every AP automation project that gets past the pilot stage. It's worth walking through why, because the fix isn't a smarter regex or a cleaner extraction model. It's a different way of thinking about what a "match" actually is.

Why Unique Identifiers Break Down at Scale

A purchase order looks like a stable anchor. It has a number, a vendor, a set of line items, quantities, and prices. An invoice references that PO number, and in theory, matching the two is just a lookup. Find the invoice's PO number, find the PO with the same number, done.

That model holds for maybe half of real-world transactions. Here's where it stops holding.

Partial deliveries. A PO for 500 units of raw material ships in three batches over six weeks because the vendor didn't have full stock on hand. Each batch generates its own goods receipt, and the vendor invoices each batch separately. One PO number now maps to three invoices and three GRs, each carrying a different partial quantity that needs to sum against the original line item without ever exceeding it. A single lookup returns three candidate rows and no way to tell the system which invoice belongs to which shipment.

Multiple invoices against one PO. Even without a partial delivery, blanket POs and standing purchase agreements generate a steady stream of invoices against a single PO number over months. A services PO for an ongoing facilities contract might see twelve monthly invoices land against it, each one identical in PO reference. The system needs to know which invoice corresponds to which billing period, and how much of the PO's total value remains unbilled after each one posts.

Line-item-level discrepancies. The PO says 1,000 units at $4.20 each. The invoice says 980 units at $4.20 because of a shipping shortage, or 1,000 units at $4.35 because of a price escalation clause buried in the contract. Header-level totals can even match by coincidence while the line items tell a completely different story, one that a header-only comparison would never catch.

Split and consolidated invoicing. A vendor might combine three separate POs into a single invoice to cut down on billing overhead. Or they split one PO's value across two invoices submitted a week apart because of a partial goods issue on their end. Neither case maps cleanly to a single PO field on the invoice, and neither is something the vendor is likely to flag or explain.

None of these are edge cases. Any AP team processing more than a few hundred invoices a month runs into all four in a given week. A matching architecture built around "look up the PO number and check if it matches" won't survive contact with that volume. Something more structural has to sit underneath it. 

This Is a Record Linkage Problem, Not a Join Problem

The instinct to treat PO-to-invoice matching as a database join comes from thinking about the two documents as if they lived in the same system with a guaranteed foreign key relationship. They don't. In almost every real deployment, PO data lives in the ERP (SAP, Oracle, NetSuite, or similar) with its own schema, its own update cadence, and its own quirks in how blanket orders and scheduling lines get represented. Invoice data comes from a completely separate pipeline, usually an intelligent document processing layer pulling structured fields out of PDFs, scanned images, or EDI feeds, where every vendor formats things slightly differently.

These are two independently populated, independently evolving datasets. The PO number field on an invoice is a strong hint about which record it might correspond to. It isn't a guaranteed key, and treating it like one is where most in-house matching builds go wrong on day one.

This is the same category of problem as entity resolution in a customer data platform, where a team tries to work out whether "J. Smith" and "John Smith, Jr." on two different lead lists are the same person. The techniques that solve that problem, fuzzy matching, weighted signals, confidence scoring, tolerance windows, are the same techniques that solve this one. An exact-match join is a useful special case for the roughly half of transactions that are simple. It was never going to be the general solution.

A graphic contrasting

Three Ways to Build the Matching Layer

Once matching gets reframed as record linkage instead of a lookup, three architectural patterns show up repeatedly. Most production systems end up using some combination of all three, but they solve different parts of the problem.

Shared join key strategy. This is the natural next step past a bare PO number lookup. Instead of matching on PO number alone, the system builds a composite key: PO number plus line item number plus release or scheduling line, often combined with a date window and a quantity tolerance. When the ERP consistently populates a scheduling line and the invoice extraction reliably captures it, this composite key resolves a good chunk of the partial delivery and recurring invoice cases that a bare PO lookup misses.

The limitation shows up in two places. First, it's still binary. A composite key either matches or it doesn't, so there's no way to express "this is probably the right invoice but the quantity is a little off, send it for review" versus "this is clearly wrong." Second, it depends entirely on extraction quality for every field in the composite key. Miss the scheduling line on one invoice out of a hundred, and that invoice falls through with no fallback logic to catch it. Composite keys are a good first filter. They're not a matching engine on their own.

Event-driven trigger architecture. Rather than running matching as a periodic batch job, this pattern treats every new document, an invoice landing, a goods receipt posting, a PO change, as an event that fires a matching attempt against the current state of the other data sources. A message queue (Kafka, RabbitMQ, SQS, or similar) carries document-processed events, and a matching service subscribes and re-evaluates the affected PO whenever something new arrives.

This solves a real operational problem: late arrivals. In a nightly batch model, an invoice that arrives an hour after the GR posts has to wait until the next run to get matched, and one that arrives before the GR has to wait even longer. An event-driven model closes that gap. The moment new information shows up, matching re-runs with the current picture rather than a stale one. What event-driven architecture doesn't solve on its own is the actual matching logic. It changes when matching runs, not how the system decides what counts as a match. Teams that build the event infrastructure without a real scoring layer underneath it end up with matching that runs instantly and is still wrong.

Dedicated matching engine with confidence scoring. This is the pattern that actually handles ambiguity instead of designing it away. A standalone service queries both extracted datasets (PO and invoice, plus GR data where three-way matching applies), computes a set of weighted signals, and returns a confidence-scored candidate match instead of a yes or no. High-confidence matches can post automatically. Mid-confidence matches route to a review queue with the specific discrepancy flagged. Low-confidence or partial matches stay open, waiting for more data, like a later shipment or a follow-up invoice, before a decision gets made.

This is the only one of the three approaches built for the reality that matching decisions often can't be made with full certainty at the moment a document arrives. It treats "we don't know yet" as a valid state instead of forcing a premature match or an unnecessary exception.

In practice, mature systems layer all three together. Composite keys act as a first-pass filter that narrows the candidate search space before any scoring runs. Event-driven triggers keep that candidate pool current as new documents arrive. The scoring engine makes the actual matching decision on whatever candidates survive the first two steps.

What Confidence Scoring Actually Requires

Building a scoring layer sounds simpler than it is until a team sits down to define what actually goes into the score. A few signals do most of the work in practice.

Exact match signals carry the most weight when they're present: PO number and line item match, vendor identifier match (tax ID is more reliable than vendor name, which varies by how a vendor fills out a field). Tolerance-based signals handle the more common near-matches: quantity delta within a defined band, often somewhere around 1 to 3 percent depending on category, unit price delta within a similar band, currency and unit-of-measure normalized before any comparison happens, since "1 case" and "24 units" need to resolve to the same quantity before a percentage comparison means anything. Contextual signals round it out: date proximity to the associated goods receipt, and remaining PO balance, since a proposed match that would push a PO line past its total ordered value is a red flag regardless of how well the other fields line up.

Two things separate a scoring system that actually works from one that generates noise. The first is category-specific tuning. A raw materials PO with a 3 percent quantity variance is often a real shipping shortage worth flagging. A services PO with the same variance might be a rounding difference in hours billed that nobody needs to see. A single global threshold treats both the same way and either buries analysts in false exceptions or lets real discrepancies through.

The second is explainability. A confidence score of 62 tells an AP analyst nothing useful. A note that reads "quantity variance 3.1 percent, above the 2 percent threshold for this category" tells them exactly what to check. Auditors need the same thing months later when they're reconstructing why a particular invoice posted automatically while a similar one sat in review. The score is the output. The reasoning behind it is what actually gets used.

There's also a question of where thresholds live. Hardcoding tolerance bands into the matching logic works fine until the first week someone in finance asks to loosen the quantity tolerance for one vendor category without touching anything else on the system. Teams that treat thresholds as configuration, stored separately from the scoring code and versioned so past decisions stay explainable, save themselves a lot of redeployment cycles down the line. It's a small design choice made early that determines how much friction a finance team feels every time a business rule needs to shift.

The State Machine Problem Hiding Inside Partial Deliveries

Partial deliveries and recurring invoices expose something a stateless join was never designed to handle: a PO isn't a single event, it's a process that plays out over weeks or months, and the system needs to track where that process stands at any given moment.

The practical fix is a ledger maintained per PO line, not per PO header. Each line tracks an ordered quantity and value, a matched-so-far quantity and value, and a remaining balance. Every new invoice or GR event that gets matched against that line decrements the remaining balance and updates a status: open, partially matched, fully matched, or over-matched if something's gone wrong. A PO line only closes out when its remaining balance hits zero, or when someone makes a deliberate decision to close it early with an unbilled balance still on it.

Without this persisted state, systems fail in one of two predictable ways. They double-count a partial shipment because nothing tracked that a portion of the line was already matched, or they never recognize that a PO is genuinely finished because there's no running total to check against. Both failures look fine in a demo with one PO and one invoice, because there's nothing to track state against yet. Both show up fast once a single PO starts accumulating invoices over its real lifecycle. This is the piece that turns matching from a stateless comparison into something closer to an accounting ledger, and it's usually the part in-house builds underestimate the most.

Diagram illustrating the internal components and data flow of a financial trading matching engine.

The Data Engineering Reality of Two Independent Sources

Everything above assumes the matching logic itself is the hard part. In production, the surrounding data engineering is often just as demanding.

Schema drift. ERP field names and structures shift with version upgrades and with custom fields that finance operations teams add over time. Invoice extraction schemas evolve too, as new vendor templates get onboarded and the extraction model picks up new field variations. A matching layer that reads directly from either source's native schema breaks every time either one changes. The fix is a normalization layer that maps both sources into a stable intermediate schema, so the matching logic itself stays isolated from changes happening upstream on either side.

Arrival order. Documents don't show up in a predictable sequence. A GR sometimes posts before the invoice, sometimes after. Services POs often never get a GR at all, since there's no physical receipt to log. A matching engine has to handle any arrival order and re-evaluate its candidate matches whenever new data lands, which is exactly why the event-driven trigger pattern matters as much as the scoring logic itself.

Idempotency. Document processing pipelines retry failed extractions as a matter of course. If a matching engine isn't built to be idempotent, a retried extraction can create a duplicate match record for an invoice that already matched successfully the first time, and now finance has two records pointing at one real transaction.

Audit trail. Finance and audit teams need to reconstruct, sometimes a year later, exactly why an invoice posted automatically or landed in an exception queue. That means every match decision, the confidence score, the specific signals that drove it, and even which version of the scoring thresholds was active at the time, needs to be logged immutably. This isn't optional for any organization that goes through a financial audit, and it's easy to bolt on as an afterthought in a way that leaves gaps exactly where auditors look first.

Build vs Buy: The Question Worth Asking

Most data and IT teams evaluating this can write the matching SQL. That was never really the hard part. The honest question is whether the team wants to own the ongoing maintenance of a stateful, confidence-scored matching layer running against two data sources that keep drifting independently of each other.

Building this in-house means committing to a normalization layer that gets updated every time the ERP or the extraction schema changes, event infrastructure that someone has to operate and monitor, a scoring model that needs periodic retuning as vendor mix shifts, an exception queue interface that AP analysts can actually work from, and audit logging that holds up under a real compliance review. None of that is a weekend project, and none of it is a one-time build either. It's ongoing infrastructure that competes for the same engineering time as everything else on the roadmap.

That's the calculus worth running before committing resources: not "can we build this," but "do we want to be the team that maintains it eighteen months from now, after the ERP upgrade and the third new vendor template format shows up." Platforms like Artificio's AP Studio exist specifically because that maintenance burden is real and ongoing, and a team that's already hardened this matching layer against thousands of vendor formats and years of edge cases has a head start that's hard to close with an internal build, even a well-executed one.

There's a middle path worth naming too. Some teams don't need a full platform swap to get out of this trap. Extraction can stay wherever it already lives, an internal IDP pipeline, an ERP add-on, whatever's already generating structured data from POs and invoices, while the matching layer itself gets pulled out as a dedicated service that both sources feed into. That separation matters because matching logic changes on a different cadence than extraction logic. Extraction gets tuned per document template. Matching gets tuned per business rule. Bundling the two together, or worse, embedding matching rules inside extraction logic, is one of the more common reasons in-house builds get harder to maintain over time rather than easier.

The Architecture Decision Happens Early

Match rate problems almost never trace back to a bad extraction model. They trace back to a matching layer that was designed for the transaction everyone imagines, one PO, one invoice, one clean join, instead of the transaction that actually shows up: partial, recurring, split across documents that arrive in whatever order the real world hands them.

Getting this right isn't about writing more matching rules. It's about deciding early whether the system treats a PO-to-invoice relationship as a lookup or as a process with state, ambiguity, and a lifecycle that plays out over time. That decision shapes everything downstream, from how confidently the system can auto-post an invoice to how quickly an AP analyst can trust what lands in their exception queue. Teams that get the architecture right once spend a lot less time firefighting match rates later. 

Share:

Category

Explore Our Latest Insights and Articles

Stay updated with the latest trends, tips, and news! Head over to our blog page to discover in-depth articles, expert advice, and inspiring stories. Whether you're looking for industry insights or practical how-tos, our blog has something for everyone.