Skip to main content

Step 4: Reconcile and Report

At the end of each day, StarLine's finance team needs to know: for each sales agent's route, how much was collected, from which distributors, and which invoices are cleared.


Query a distributor's statement

const { kanall } = require('./kanall')

const statement = await kanall('GET', '/v1/accounts/distributor-emeka/statement')

console.log(statement.closingBalance) // "63500.00"
console.log(statement.totalCredits) // "63500.00"

// Count only confirmed entries
const confirmedBalance = statement.lines
.filter(l => l.entry.Status === 'confirmed' && l.entry.Direction === 'credit')
.reduce((sum, l) => sum + parseFloat(l.entry.Amount), 0)

console.log(confirmedBalance) // 45000

Sample statement response:

{
"virtualAccount": { "AccountRef": "distributor-emeka", "Status": "active" },
"lines": [
{
"entry": {
"Direction": "credit",
"Amount": "45000.00",
"Fee": "50.00",
"Status": "confirmed",
"Narration": "Transfer from Emeka Okafor",
"CreatedAt": "2026-07-07T11:00:00Z"
},
"runningBalance": "45000.00"
},
{
"entry": {
"Direction": "credit",
"Amount": "18500.00",
"Fee": "25.00",
"Status": "provisional",
"Narration": "Transfer from Emeka Okafor",
"CreatedAt": "2026-07-07T16:30:00Z"
},
"runningBalance": "63500.00"
}
],
"openingBalance": "0.00",
"totalCredits": "63500.00",
"totalDebits": "0.00",
"closingBalance": "63500.00"
}

The closingBalance includes both confirmed and provisional entries. For conservative reconciliation where you only count what Nomba has verified, filter to confirmed only.

:::caution Parse amounts as Decimal Amount, closingBalance, and all other monetary fields are decimal strings ("45000.00"). Always parse them as Decimal / BigDecimal — never float or double, which can introduce rounding errors in financial calculations. :::


End-of-day route report

Generate a collection summary for each sales agent's assigned distributors:

const { kanall } = require('./kanall')

async function generateRouteReport(agentId) {
const distributors = await db.query(
'SELECT id, name, kanall_ref FROM distributors WHERE agent_id = $1',
[agentId]
)

const report = []
const today = new Date().toDateString()

for (const distributor of distributors.rows) {
if (!distributor.kanall_ref) continue

const statement = await kanall('GET', `/v1/accounts/${distributor.kanall_ref}/statement`)

const todayLines = statement.lines.filter(
l => new Date(l.entry.CreatedAt).toDateString() === today
)

const confirmedToday = todayLines
.filter(l => l.entry.Status === 'confirmed' && l.entry.Direction === 'credit')
.reduce((sum, l) => sum + parseFloat(l.entry.Amount), 0)

const pendingToday = todayLines
.filter(l => l.entry.Status === 'provisional')
.reduce((sum, l) => sum + parseFloat(l.entry.Amount), 0)

report.push({
distributor: distributor.name,
accountRef: distributor.kanall_ref,
confirmedToday,
pendingToday,
runningBalance: parseFloat(statement.closingBalance),
})
}

return {
agentId,
date: today,
distributors: report,
totalConfirmed: report.reduce((sum, r) => sum + r.confirmedToday, 0),
totalPending: report.reduce((sum, r) => sum + r.pendingToday, 0),
}
}

Sample output:

{
"agentId": "agent-007",
"date": "2026-07-07",
"distributors": [
{ "distributor": "Emeka Okafor", "confirmedToday": "45000.00", "pendingToday": "18500.00", "runningBalance": "63500.00" },
{ "distributor": "Fatima Yusuf", "confirmedToday": "32000.00", "pendingToday": "0.00", "runningBalance": "32000.00" },
{ "distributor": "Chukwudi Eze", "confirmedToday": "0.00", "pendingToday": "0.00", "runningBalance": "0.00" }
],
"totalConfirmed": "77000.00",
"totalPending": "18500.00"
}

What to do with each status

StatusMeaningAction
confirmedNomba has verified the paymentClear the invoice. Release credit hold. Allow next delivery.
provisionalPayment received but not yet verifiedShow as "awaiting bank confirmation". Re-run the report in 15 minutes.
reversedPayment was not confirmed by NombaDo not clear the invoice. Flag the distributor for follow-up.

Handling long account histories (pagination)

For distributors with many transactions, the statement is cursor-paginated. Loop until pagination.hasMore is false, passing ?after={nextCursor} on each subsequent request:

async function getAllEntries(accountRef) {
const entries = []
let cursor = null

do {
const path = `/v1/accounts/${accountRef}/statement${cursor ? `?after=${cursor}` : ''}`
const page = await kanall('GET', path)
entries.push(...page.lines)
cursor = page.pagination?.hasMore ? page.pagination.nextCursor : null
} while (cursor)

return entries
}

The same cursor loop applies in Python, Go, and Java — replace the kanall() call with your language's client.


Summary

You have built a complete FMCG payment collection system on Kanall:

StepWhat it does
Per-distributor NUBANsNo more shared accounts — every payment lands with perfect attribution
Webhook handlerReal-time notification when funds arrive — invoices cleared automatically
Statement reconciliationThe ledger tells you what is real, not just what was claimed
Route reportFinance gets a clean daily summary without touching a spreadsheet

This same pattern applies to any domain: logistics, savings groups, school fees, marketplaces. The primitive is the same — only your business logic changes.


Next: Settlement — move money out →