Skip to content
SkanPay Docs

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:

reconcile.mjs
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:

GET /v1/transactions/chg_… (abridged)
{
  "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 accountWhat it is to you
MERCHANT_PENDINGYour pending balance: collected, not yet released.
MERCHANT_AVAILABLEYour available balance: spendable on payouts and withdrawals.
FEES_REVENUESkanPay's fee on the transaction.
PROVIDER_FLOATMoney held at the mobile money network.
SETTLEMENT_CLEARINGMoney 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.