Part 5 of 7 · Supplier bill matcher series ~5 min read

How a bill gets approved for payment

A bill lands in Dan’s inbox at 8:03am. Packaging Co, $4,200, flagged price variance. There are three buttons: Approve, Query, Reject. What happens when he taps one? The honest answer is “it depends which one.” This post walks through the three things an approver can do — approve, query, reject — and how the bill, the supplier, and the audit trail all stay in sync. The one thing none of them does is pay the bill: that’s always your normal payment run, after a human has said yes.

Key takeaways

  • Three actions per bill: approve (clear for the payment run), query (ask the supplier), reject (decline).
  • Approving a flagged bill requires a reason, captured in the audit trail.
  • Query sends a templated note to the supplier; a reissued bill re-enters the matcher fresh.
  • Approve only marks the bill ready — payment is still your normal run, never the matcher.
  • The Approve button is an email action backed by a Function URL.

Three actions on a bill

Three actions an approver can take on a bill A diagram showing one input on the left flowing through an approval email, then branching into three action paths. Far left: a "Bill in approver inbox" box showing a typical email — supplier, total, the matched lines or the exact failing line — with three button placeholders below: Approve, Query, Reject. The approver taps one button. The middle column shows the three branches. Branch one, Approve: on a clean matched bill it's one tap; on a flagged bill a small form asks for a reason for overriding the flag. A Function URL Lambda marks the bill ready_for_payment in the bm-bills table, closes the matched purchase-order line, and writes an approved event to the audit trail. The bill joins the next payment run — the matcher never moves money itself. Branch two, Query: opens a small form with a templated question to the supplier (confirm the agreed price, or reissue for the received quantity); on send, a Function URL Lambda emails the supplier via SES outbound and parks the bill in a waiting_on_supplier state. A reissued bill re-enters the reader and the matcher fresh. Branch three, Reject: declines the bill with a reason; the Function URL Lambda marks it rejected, notifies the supplier with the reason, and closes it out — nothing is queued for payment. The right side shows the convergence: every action writes a row to the bm-audit DynamoDB table with timestamp, bill id, action, by-user, and notes. A note at the bottom: approve only marks a bill ready — payment is always your normal run, after a human has said yes. Bill in inbox supplier, total, lines [Approve] [Query] [Reject] Action 1 Approve • Clean: one tap. Flagged: a reason for override • Marks bill ready_for_payment • Closes the PO line Action 2 Query • Templated note to supplier (confirm price / reissue) • Parks bill waiting_on_supplier; a reissue re-enters fresh Action 3 Reject • Decline the bill with a reason • Supplier notified — nothing queued to pay Audit trail DynamoDB bm-audit timestamp · bill_id action · by-user notes Approve only marks a bill ready — payment is always your normal run, after a human says yes.
Fig 5. Three actions per bill, three different effects. Approve clears it for the payment run. Query asks the supplier and parks the bill. Reject declines it. Every action writes to the audit trail, and none of them moves money.

Action 1: approve (the most common)

Most bills are clean, so most of the time Approve is a single tap. On a matched bill, the approver glances at the supplier and total, taps Approve, and they’re done. On a flagged bill, Approve is slightly heavier on purpose: a small form opens asking for a reason for overriding the flag — “agreed the higher price with the supplier by phone” or “short delivery, but we’ll accept and they’ll credit next month.” The reason is required, because an override with no explanation is exactly the thing an auditor (or a future you) will want to understand later.

The button submits to the ack-handler Function URL. Three things happen, in order. First, the bill is marked ready_for_payment in the bm-bills table and the matched purchase-order line is closed so it can’t be billed against twice. Second, if the bill needed two sign-offs (Part 4), the bill stays in a half-approved state until the second approver also taps Approve. Third, an action: approved row is written to bm-audit with the user, timestamp, outcome, and any override reason.

Marking a bill ready_for_payment does not pay it. The matcher hands a clean list of approved bills to your normal payment process — an export to your accounting tool, or a file for the bank run — and a person runs that process. The matcher never touches money. That boundary is the whole point: the system removes the tedious checking, not the human judgment about whether to actually pay.

Action 2: query (the “ask the supplier”)

When a bill is flagged and the approver doesn’t want to either approve or reject it — they want the supplier to explain or fix it — they tap Query. A small form opens with a templated message tailored to the outcome: for a price variance, “Your bill charges $4.20/unit; our PO agreed $3.80/unit. Please confirm the price or reissue.” For a quantity variance, “Your bill is for 100 units; our dock received 84. Please reissue for the received quantity or advise on the balance.”

On send, the ack-handler emails the supplier via SES outbound and parks the bill in a waiting_on_supplier state. The bill stops chasing the approver — the ball is in the supplier’s court. If the supplier reissues, the corrected bill comes back in through the normal intake lanes, gets read fresh, and is matched again from scratch. A corrected bill that now matches clean clears for a one-tap approval, and the loop closes. The original queried bill is linked to the reissue in the audit trail so the history is one connected story, not two unrelated bills.

Action 3: reject (the decline)

Sometimes the right answer is no. A duplicate of a bill already paid. A bill for an order that was cancelled. A no-PO bill for goods nobody ordered. The approver taps Reject, gives a reason, and the ack-handler marks the bill rejected, notifies the supplier with the reason, and closes it out. Nothing is queued for payment, and the rejected bill stays in the record so the same bill arriving again can be spotted as a re-send rather than treated as new.

Reject is deliberately final but not destructive. The bill isn’t deleted — it’s kept, marked rejected, with the reason and the person attached. If a reject was a mistake (the duplicate turned out to be a legitimate second order), an admin can reopen it through a small command that reads the audit snapshot and puts the bill back in the queue. The reopen is itself an audit row.

Every action is logged, every action is reversible

The bm-audit table records every approve, query, and reject with the user who acted, the timestamp, the outcome the matcher had assigned, any override reason, and a snapshot of the bill’s state before and after. If a bill was approved in error — wrong supplier, wrong amount — an admin can run an “undo last action” that restores the previous state from the snapshot, as long as the bill hasn’t already gone out in a payment run. The undo is itself an audit row, so the trail of edits stays clean.

This kind of reversibility matters most for the decisions made quickly on a busy morning. Six months later, when an auditor or the owner asks “why did we pay this one over the PO price?”, the answer is right there: who approved it, when, and the reason they typed. The audit trail is the system’s memory.

Next post: the cost breakdown. The whole pipeline above runs in coffee-money territory at SMB volume; Part 6 explains exactly where the dollars go and why the match itself is essentially free.

All posts