An invoice dispute triager on AWS for a few dollars a month
A customer says an invoice is wrong. What happens next in most small businesses is a thread: somebody in accounts asks what specifically is wrong, the customer replies three days later, accounts asks operations for the delivery note, operations finds it a day later, accounts replies, and eleven days after the original email an invoice that was correct all along gets paid. Meanwhile the automated reminder has gone out twice. This post walks through a small system that does the gathering first.

Key takeaways
- Six standard reasons cover almost every dispute, and each needs different evidence.
- The evidence is gathered before a human opens it, not after.
- A dispute pauses automatic chasing immediately. Nothing sours a customer faster.
- Not every dispute is about the invoice. “Unhappy with the work” goes to the relationship owner.
- Designed on AWS for about $3 a month at typical small-business volume.
The whole system on one page
Before any code, here is the shape of what we are designing.
- App integration
- Machine learning
- Analytics
- People
The six reasons
| Reason | What the customer means | What it needs |
|---|---|---|
| Quantity | We did not receive that many | The delivery note, signed |
| Price | That is not what we agreed | The quote or the price list on that date |
| Duplicate | We have already paid this | The payment history and the other invoice |
| Never ordered | We did not order this at all | The purchase order or the job record |
| Wrong entity | This should go to our other company | The account setup and the contract |
| Not the invoice | We are unhappy with the work | Nothing — it goes to a person, not accounts |
Those six cover the overwhelming majority of disputes in a small business, and the important property of the list is that the evidence column is completely different for each. That is the whole reason classifying is worth doing: it turns “find out what they mean, then find the relevant paperwork” into one automatic step.
What runs on every dispute (the inside)
- The classifier. Reads the message and picks one of the six, or says it cannot tell. This is the one place a model earns its keep — customers do not write “I am raising a quantity dispute”, they write “only 6 of the 10 turned up”. It also extracts the invoice number, which is usually in the message and occasionally is not.
- The gatherer. Fetches exactly what the reason needs. For quantity, the delivery note and the picking record. For price, the quote and the price list as it stood on the invoice date. For duplicate, every payment against that customer in the period plus any invoice with a similar total. Nothing else, because a decision buried in twelve attachments is not a decision.
- The router. Flags the invoice as disputed so the chaser stops, and sends the assembled decision to the right person. Five of the six go to accounts. The sixth — the one that is not really about the invoice — goes to whoever owns the relationship, which is the single most valuable routing decision the system makes.
One dispute, end to end
- App integration
- Machine learning
- Security & identity
- Analytics
- People
In plain words
A customer emails: “Invoice 4412 — we only got 6 of the pallets, not 10.” The classifier reads that as a quantity dispute on invoice 4412. Within seconds the invoice is flagged disputed, so the reminder scheduled for Friday does not go. The gatherer pulls the delivery note for that invoice, which is signed, and the picking record, which says 10 pallets loaded on two vehicles. It attaches both, plus the photograph of the signed POD, and sends one message to accounts: quantity dispute, invoice 4412, POD signed for 10, second vehicle delivered 14:20.
Somebody in accounts opens that, sees a signed delivery note for ten, and replies to the customer with the POD attached in under two minutes. Nine times out of ten the reply comes back “ah, the second load came separately, sorry”, and the invoice is paid. The tenth time it turns out the second vehicle went to the wrong site, which is an operational problem the business genuinely needed to know about — and it now knows about it on the same day rather than in eleven.
Design rules that shaped every decision
- Pause the chaser first, understand second. A dunning reminder during a genuine dispute does more damage than any invoice error.
- Gather narrowly. The evidence for the claimed reason and nothing else; twelve attachments is not a decision.
- The system never issues a credit note, never adjusts an amount, and never replies to the customer on its own.
- “Not about the invoice” is a first-class classification, and routing it away from accounts is the highest-value thing the system does.
- An unclassifiable dispute goes to a person with the raw message. Guessing a reason gathers the wrong evidence, which is worse than gathering none.
- Every decision is recorded against the customer, because the same dispute reason recurring is a process problem rather than a customer problem.
Why this shape
Disputes are expensive out of all proportion to their number. A business raising four hundred invoices a month might see fifteen disputes, and those fifteen will consume more accounts time than the other three hundred and eighty-five combined — not because they are hard, but because each one is a multi-day thread with waiting in it. The waiting is the cost, and almost all of it is waiting for information that already exists somewhere in the business.
So the design front-loads the gathering. It accepts that a model will occasionally classify wrongly, which costs one round trip, in exchange for the far larger saving of having the right paperwork attached the first time in the other ninety per cent. And it does the one thing that has to be instant — stopping the chaser — before it tries to understand anything.
The next four posts walk through each piece: how a dispute is recognised, how it gets classified, how the evidence is gathered, and how a decision gets recorded. One diagram per post, a cost breakdown, and an engineering reference at the end.
All posts