KibiPay
HomeBlog › Reconciliation

Four Kinds of Reconciliation Break: Status-Mismatch, Stuck, Unreconcilable, Position-Drift

7 min read Reconciliation
ReconciliationOperations
Four Kinds of Reconciliation Break: Status-Mismatch, Stuck, Unreconcilable, Position-Drift

When a reconciliation sweep finds that reality and the ledger disagree, "there's a discrepancy" isn't a useful thing to tell an operator. Why they disagree determines what to do about it. KibiPay's engine classifies every break into one of four kinds, and the kind is the most important field on the break, because it encodes the failure mode.

STATUS_MISMATCH — the scheme knows something we don't

This is the happy kind of break, if there is one. The engine asks the rail's adapter for the payment's status, and the adapter comes back with a terminal answer — settled, rejected, returned — that differs from our non-terminal record. In other words, the payment finished, but the callback that should have told us got lost. The scheme has the truth; we're simply behind. Because the outcome is unambiguous (a terminal state from the authoritative source), this is the one kind the engine can often fix on its own — see auto-applying a lost callback. Its dedupe key is STATUS_MISMATCH:{payment_id}.

STUCK — nobody has finished

Here the engine asks and the scheme answers, but the answer isn't terminal either. Our side is in-flight, the scheme's side is in-flight (or silent, reported as unknown), and the payment has been sitting past its grace window with no resolution from anyone. Nothing is wrong exactly, but nothing is progressing, and a payment that's been "submitted" for hours needs a human to chase it. There is no automatic remedy — you can't invent a terminal state that doesn't exist yet — so a STUCK break (keyed STUCK:{payment_id}) is always a prompt for investigation.

UNRECONCILABLE — we can't even ask

Some rails simply don't offer a status-enquiry surface. If a payment is on such a rail and drifts, the engine can't query the scheme to find out what happened, because there's nowhere to query. Rather than pretend, it raises an UNRECONCILABLE break (keyed UNRECONCILABLE:{payment_id}) whose detail records that the scheme "has no status-enquiry endpoint." This is an honest break: it says "a human will have to establish the truth out-of-band, because automated enquiry isn't possible here." Naming it prevents the worse outcome of silently assuming everything's fine on a rail you can't actually check.

POSITION_DRIFT — the books don't tie out

The first three kinds are about individual payments. This one is about aggregate positions. Separately from payment-level checks, the engine asks treasury for each rail-and-currency position and compares our ledger's view against the external view. Where they diverge beyond tolerance, it raises a POSITION_DRIFT break keyed by rail and currency (POSITION_DRIFT:{rail}:{currency}), recording the ledger position, the external position, and the drift amount. This is the break that catches problems no single payment reveals — a slow accumulation of small errors, a missed fee, an off-by-one in a batch — because it looks at the whole rather than the parts. It's explored further in ledger-vs-scheme versus ledger-vs-external.

Why a taxonomy beats a flag

You could imagine a system with a single boolean: reconciled, or not. It would be almost useless, because "not reconciled" bundles together four completely different situations with four completely different responses — apply the missing update, chase a stuck payment, investigate a rail you can't query, or hunt down a position error. By making the kind a first-class field, the queue can be filtered by kind, summarized by kind on a dashboard, and routed to the right person. The classification is the diagnosis, and the diagnosis is most of the work.

From a kind to a triage queue

The reason the kind is a first-class field rather than a note buried in a detail blob is that it drives everything downstream. The queue's summary endpoint returns open counts by kind, which is exactly the breakdown you'd put on an operations dashboard — twelve stuck, three unreconcilable, one position-drift tells you at a glance where the day's work is. The list endpoint can be filtered by kind, so the person who chases stuck payments and the person who investigates position drift each see only their queue. And the kind encodes automatability: only a STATUS_MISMATCH, with its authoritative terminal answer, is a candidate for automatic resolution — the other three always route to a human. So the classification isn't bookkeeping; it's the routing table. A break's kind decides whether a machine can close it, which dashboard tile it increments, and which operator's queue it lands in — three consequential decisions, all made the moment the sweep names the failure mode.

See it in motion

KibiPay connects UK Faster Payments, Bacs, CHAPS, Mojaloop mobile money, a mock ACH rail and Solana behind one API, with a cross-rail alias directory, ISO 20022 messaging and real-time fraud & AML screening.

Open the live console Directory demo