Reconciliation
Every incoming transaction has a reconciliation status:
| Value | Meaning |
|---|---|
matched |
Linked to a payment request or an order ref |
pending |
Needs a human – usually a wrong transfer description |
unmatched |
Confirmed not an order (e.g. a personal transfer, a refund) |
Auto-matching
Section titled “Auto-matching”Hensa looks for the referenceCode (e.g. LK7F3K9) in the transfer description. Found with the right amount → matched and payment_request.paid is emitted. Nothing else to do.
Manual matching
Section titled “Manual matching”When a transaction is pending, link it to the right order in one of three ways:
# 1. To a Hensa payment requestcurl -X POST https://api.hensa.example/v1/bank_transactions/btx_…/reconcile \ -H "Authorization: Bearer $HENSA_API_KEY" -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" -d '{ "paymentRequestId": "prq_…" }'
# 2. To an order id in your own system -d '{ "orderRef": "DH-1024" }'
# 3. Mark it as not an order -d '{ "notAnOrder": true }'Store staff can do the same in Dashboard › Transactions › To match – the result is identical.
Safety net
Section titled “Safety net”Webhooks are the primary path, but networks and servers fail sometimes. Run a periodic job (e.g. every 15 minutes) that fetches new transactions and checks them against your orders:
let cursor = nulldo { const url = new URL('https://api.hensa.example/v1/bank_transactions') url.searchParams.set('limit', '100') if (cursor) url.searchParams.set('cursor', cursor) const page = await (await fetch(url, { headers: { Authorization: `Bearer ${process.env.HENSA_API_KEY}` } })).json() for (const tx of page.data) handle(tx) cursor = page.nextCursor} while (cursor)Anything you haven’t recorded yet → handle it as if the webhook just arrived. Because your handling is idempotent, running it again is always safe.