Reconciliation
Match every SkanPay transaction to your own records, and account for every shilling in your balance.
Your join key is reference
Every transaction carries the reference you sent, unchanged. Store SkanPay's id on your order as well, and you can join in both directions.
A daily reconciliation job
Webhooks keep you up to date in real time. A nightly job catches anything missed, for instance while your server was down for longer than the retry window:
const auth = { Authorization: `Bearer ${process.env.SKANPAY_SECRET_KEY}` };
const since = new Date(Date.now() - 2 * 24 * 3600 * 1000); // look back two days
let cursor;
outer: do {
const url = new URL("https://api.skanpay.website/v1/transactions");
url.searchParams.set("limit", "100");
if (cursor) url.searchParams.set("starting_after", cursor);
const page = await (await fetch(url, { headers: auth })).json();
for (const t of page.data) {
if (new Date(t.created_at) < since) break outer; // newest first, so stop here
if (t.type !== "CHARGE") continue;
const order = await findOrderByReference(t.reference); // your database
if (!order) { console.warn("unknown reference", t.reference, t.id); continue; }
if (t.status === "SUCCESSFUL" && order.status !== "paid") {
console.warn("paid at SkanPay but not in our shop", t.reference, t.id);
}
if (t.status !== "SUCCESSFUL" && order.status === "paid") {
console.warn("paid in our shop but not at SkanPay", t.reference, t.id, t.status);
}
}
cursor = page.has_more ? page.next_cursor : undefined;
} while (cursor);Explaining your balance
Your balance is derived from a double-entry ledger; there is no number that is simply edited. Every transaction's ledger entries are returned by GET /v1/transactions/:id, next to its full status history:
{
"transaction": { "id": "chg_…", "type": "CHARGE", "status": "SUCCESSFUL", "amount": "50000", "fee": "1250", "net": "48750", ... },
"events": [
{ "status": "INITIATED", "source": "API", "created_at": "…" },
{ "status": "PENDING_PROVIDER", "source": "API", "created_at": "…" },
{ "status": "AWAITING_CUSTOMER", "source": "API", "created_at": "…" },
{ "status": "SUCCESSFUL", "source": "PROVIDER_CALLBACK", "created_at": "…" }
],
"ledger": [
{ "account": "PROVIDER_FLOAT", "side": "DEBIT", "amount": "50000", "currency": "UGX", "reason": "CHARGE_SETTLED" },
{ "account": "MERCHANT_PENDING", "side": "CREDIT", "amount": "48750", "currency": "UGX", "reason": "CHARGE_SETTLED" },
{ "account": "FEES_REVENUE", "side": "CREDIT", "amount": "1250", "currency": "UGX", "reason": "CHARGE_SETTLED" },
{ "account": "MERCHANT_PENDING", "side": "DEBIT", "amount": "48750", "currency": "UGX", "reason": "CHARGE_SETTLED" },
{ "account": "MERCHANT_AVAILABLE", "side": "CREDIT", "amount": "48750", "currency": "UGX", "reason": "CHARGE_SETTLED" }
]
}| Ledger account | What it is to you |
|---|---|
MERCHANT_PENDING | Your pending balance: collected, not yet released. |
MERCHANT_AVAILABLE | Your available balance: spendable on payouts and withdrawals. |
FEES_REVENUE | SkanPay's fee on the transaction. |
PROVIDER_FLOAT | Money held at the mobile money network. |
SETTLEMENT_CLEARING | Money on its way out to a wallet. |
A credit to a MERCHANT_* account increases your balance; a debit decreases it. Debits always equal credits within each entry, so everything adds up.
In the dashboard
Transactions shows the same timeline and ledger for every transaction. The dashboard and the API read the same data: what you see in one, you get from the other. The last two ledger lines above are the next-day release from pending to available, recorded against the same transaction.