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 →