Billing Reconciliation Spec: Tolerance and Exceptions
I have shipped two reconciliation systems and inherited four. The short version is this: if your internal ledger does not reconcile to exactly zero against itself, you have a bug, not a rounding problem. Tolerance is for the external world, where processors and banks round on their own clocks. The spec exists to draw that line and hold it under audit pressure.
What "Day Closed" Actually Means
Most teams say the day is closed when the reconciliation job finishes. That is operator language, not spec language. In the spec, a closed day means three things are true: every order with a payment intent has a terminal state, every row in the internal payments table has a matched settlement line or an explicit exception reason, and the general ledger posting has been written and locked. Nothing moves money after close without a dated adjustment.
You cannot safely release funds to a merchant, issue a refund against yesterday, or recognize revenue until the day is closed in this stricter sense. I have watched a team treat "job finished" as close and then silently post negative adjustments the next morning because a processor pushed a late capture. The auditors noticed before the CFO did.
Two Tolerances, Not One
The single biggest spec mistake I see is a global tolerance of "one cent" applied everywhere. That is wrong. You need two tolerances, and they live in different parts of the pipeline.
- Internal ledger vs internal ledger: exactly $0.00. Payments table to orders table to tax system must reconcile to the penny. If they do not, something is wrong with your own code, and hiding it behind a tolerance is how finance teams end up with phantom revenue.
- Internal ledger vs external settlement: cents-level, usually $0.01 per line and $1.00 per batch of 1,000. Processors round FX and fees on their own schedules. A one-cent gap on a single transaction is not a bug. A one-cent gap on every transaction is.
Write both numbers into the spec and say why. Reviewers who have not lived through a finance escalation will push back on the zero-tolerance rule. Hold the line.
The $127.03 Problem
A concrete case from last year. A Stripe settlement line reads $127.00. The matching internal payment reads $127.03. The naive reconciler flags it "within tolerance" because the file had a $0.05 cushion, and moves on.
Three cents turned out to be a tax rounding bug. We were computing tax on the subtotal, rounding to the cent, then summing. The processor computed tax on the line total and rounded once. Over six months the drift reached about $4,200 across 140,000 transactions. Nothing caught it because every individual mismatch was "within tolerance." The spec rule that came out of this: tolerance applies to processor fees and FX. Tax, principal, and refunds match to zero or they go to the exception queue.
Exception Queues With Separate Playbooks
A single exception queue is a graveyard. I want four, each with its own on-call runbook and aging SLA:
- Timing exceptions. Charge exists internally, settlement has not arrived yet. Auto-resolve window: 5 business days. After that, escalate.
- Missing exceptions. Settlement line exists, no internal record. These are the scary ones. Investigate within 24 hours; they often indicate a dropped webhook or a duplicate-suppression bug.
- Duplicate exceptions. Same external reference matched to two internal rows, or vice versa. Usually a retry bug. Freeze the affected code path until root cause is known.
- Amount-diff exceptions. Both sides exist, numbers disagree. Bucket by size: under $0.05 on a fee line is probably processor rounding; anything on a principal line is a bug.
Pairwise Sources, Not a Mega-Join
The spec should name exactly four reconciliation sources and describe the pairs between them. In my systems those are: the processor settlement file, the internal payments table, the internal orders table, and the tax system. Do not try to reconcile all four in one SQL statement. Run three pairwise jobs.
- Payments to orders. Every captured payment has exactly one order; totals match.
- Payments to settlement. Every payment appears in a settlement batch within the late-arrival window, or moves to the timing queue.
- Orders to tax. Every order's tax line matches the tax system's recorded liability for the same period.
If all three pairs reconcile, the four-way reconciliation is implied. If one fails, you know exactly which interface is broken. A mega-join tells you there is a problem somewhere in a cloud of rows.
FX, Late Arrivals, and Chargebacks
Multi-currency adds three spec questions people try to dodge. Answer them explicitly: which rate, mid-market or processor-reported? At what timestamp, capture or settlement? Who absorbs slippage, the merchant or the platform? I default to processor-reported rate at settlement time, platform absorbs slippage up to 10 basis points, anything beyond routes to an FX exception queue.
Late arrivals are a separate beast. A charge authorized on day N can settle on day N+3 because of processor backlog. The spec must define the late-arrival window (I use 7 calendar days) and what happens on day 8: no longer eligible for auto-match, manual review required. Anything you do not define here, a junior engineer will define for you with a quiet timeout and a silent data-loss bug.
Chargebacks and disputes are not part of daily reconciliation. They move on a different clock and need their own flow against the dispute system. Lumping them into daily is how you get a "daily" job that never closes cleanly.
Who Can Zero Out $47?
Every reconciliation system eventually has a tiny residual that nobody can explain. The spec must answer: who has the authority to write it off? My rule: anything under $10 per month in aggregate can be written off by the reconciliation lead with a logged reason code. $10 to $500 requires finance approval. Over $500 requires an incident and a root-cause writeup before the write-off is booked. No write-off happens without a dated journal entry and a named approver. This is boring, and it is the single clause auditors care about most.
Observability That Actually Signals
Four metrics, published daily:
- Match rate: percentage of settlement lines auto-matched on first pass. Target 99.5% or higher. A drop of half a point overnight is a P1.
- Exception aging: count of open exceptions by queue and by days-open. If timing exceptions over 5 days are growing, something in the settlement feed is stuck.
- Auto-resolve rate: percentage of exceptions that clear without human touch within the SLA window. Falling auto-resolve rate is an early warning of a new bug class.
- Write-off volume: dollars written off per week. Should be flat and tiny. A spike is almost always a new bug being papered over.
Acceptance Criteria
- Given a processor settlement file for 2026-04-16 And all internal payments for that settlement date When the daily reconciliation job runs Then every settlement line matches an internal payment within $0.01 And every internal payment is either matched or routed to a named exception queue And the internal payments-to-orders pair reconciles to exactly $0.00 And the day is marked closed only if all three conditions hold - Given an amount-diff exception of $0.03 on a principal line When the reconciler evaluates tolerance Then the exception is NOT auto-resolved And it is routed to the amount-diff queue with a 24-hour SLA - Given a settlement line that arrives 8 calendar days after the capture date When the late-arrival matcher runs Then the line is flagged for manual review And it does NOT auto-match even if the amounts agree
These are the clauses I want signed off before anyone writes a line of SQL. If the reviewers cannot agree on them in a meeting, the system is not ready to be built, no matter how clean the architecture diagram looks.
Contract Review Packet to Copy
Use this when the work touches API behavior, schema, events, retries, or consumer expectations. The packet makes compatibility and release evidence explicit.
API contract review packet: Billing Reconciliation Spec: Tolerance and Exceptions Decision to make: - Writing a billing reconciliation spec: tolerance thresholds, exception queues, the daily close process, and why penny-level differences are usually a bug not a rounding artifact. Owner check: - Product owner: - Engineering owner: - QA or operations reviewer: Scope boundary: - In scope: - Out of scope: - Assumption that still needs approval: Acceptance evidence: - Test or fixture: - Log, metric, or screenshot: - Manual review step: Contract boundary: no release without compatibility classification, consumer impact, retry behavior, and rollback notes. Reviewer prompt: - What would still be ambiguous to someone who missed the planning meeting? - What evidence would make this safe enough to ship?
Editorial Review Note
Reviewed Apr 28, 2026. This update added a reusable artifact, checked the article against the related topic hub, and tightened the next-step links so the page works as a practical reference rather than a standalone essay.
Topic Path
This article belongs to the API Contracts track. Start with the hub, then use the checklist, template, or tool below on a real project.
Keep Reading
Editorial Note
Last reviewed Apr 28, 2026: examples, internal links, and reusable review blocks were checked for practical specificity.
- Author details: Spec Coding Editorial Team
- Editorial policy: How we review and update articles
- Corrections: Contact the editor