Reconciliation patterns
Patterns for keeping your ledger and ours in agreement, from real-time webhooks to daily statements.
Pattern 1: event-driven
Treat credit.settled as the source of truth for availability and credit.returned for reversals. Store the credit id and make your handler idempotent: an event may be delivered more than once.
app.post("/neo/webhooks", async (req, res) => {
if (!verifyNeoSignature(req.rawBody, req.get("NEO-Signature"), process.env.NEO_WEBHOOK_SECRET)) {
return res.status(400).end();
}
const event = JSON.parse(req.rawBody);
const seen = await db.events.exists(event.id);
if (seen) return res.status(200).end();
switch (event.type) {
case "credit.settled":
await ledger.credit(event.data.external_ref, event.data.amount, event.data.currency, event.data.id);
break;
case "credit.returned":
await ledger.noteReturn(event.data.external_ref, event.data.id, event.data.return_reason);
break;
}
await db.events.insert(event.id);
res.status(200).end();
});
Pattern 2: daily statement reconciliation
Each morning, fetch the previous day's statement per account (or the consolidated statement for your programme) and compare entry by entry with what your webhooks recorded. Any difference is a missed or duplicated event, which you can resolve with GET /events and POST /events/{id}/redeliver.
Pattern 3: amount matching for invoices
When one IBAN serves one customer with many invoices, match on amount within a tolerance, then on remittance_info where present. Partial payments appear as separate credits; sum them against the open invoice.
Pattern 4: balance checks
Use GET /balances as a sanity check, not as a ledger. If your computed balance diverges from ours, replay events from the last agreed point rather than overwriting.
Ordering
Events may arrive out of order after retries. Always read created_at and the resource's current state, and never assume credit.settled follows credit.received in delivery order.