Skip to main content

Step 5: Settle — Move Money Out

At the end of the week, StarLine Gas pays each distributor their collected balance. Settlement is an outbound bank transfer from a virtual account to any Nigerian bank account.

Before settling, always:

  1. Look up the recipient's bank account name to confirm the details
  2. Check the account balance to confirm it covers the transfer amount
  3. Initiate the transfer

Step A — List available banks

Get the full list of supported banks and their codes. Bank codes are required for the settle request.

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

const { banks } = await kanall('GET', '/v1/transfers/banks')

const firstBank = banks.find(b => b.name.toLowerCase().includes('first bank'))

Response:

{
"banks": [
{ "name": "First Bank of Nigeria", "code": "011" },
{ "name": "Guaranty Trust Bank", "code": "058" },
{ "name": "Access Bank", "code": "044" }
]
}

Step B — Verify the recipient account

Resolve an account number to its registered name before initiating a transfer. This protects against typos and avoids sending to the wrong person.

const lookup = await kanall('POST', '/v1/transfers/lookup', {
bankCode: '011',
accountNumber: '3201467801',
})

Response:

{
"accountName": "Akalonu Chukwuduzie Blaise",
"accountNumber": "3201467801",
"bankCode": "011"
}

Confirm accountName matches who you intend to pay before proceeding.


Step C — Check the account balance

Confirm the virtual account has enough confirmed balance to cover the transfer amount.

const { balance } = await kanall('GET', '/v1/accounts/distributor-emeka/balance')

const transferAmount = '45000.00'
if (parseFloat(balance) < parseFloat(transferAmount)) {
throw new Error('Insufficient balance')
}

Response:

{ "balance": "45000.00" }

Step D — Initiate the settlement

const result = await kanall('POST', '/v1/accounts/distributor-emeka/settle', {
amount: '45000.00',
bankCode: '011',
accountNumber: '3201467801',
narration: 'Emeka payout - week 28',
})

await db.query(
'UPDATE settlements SET merchant_tx_ref = $1 WHERE distributor_id = $2',
[result.merchantTxRef, distributorId]
)

Response: 202 Accepted

{
"merchantTxRef": "knl_1751500000_abc12345",
"status": "pending"
}

merchantTxRef is a server-generated reference. You do not provide this — Kanall generates and returns it. Store it immediately; it is the only way to track this transfer later.

:::caution amount must be a string Send "amount": "45000.00" — a decimal string. Sending "amount": 45000 (a number) will return 422 Unprocessable Entity. :::


Step E — Poll the transfer status

The settle endpoint returns 202 — the transfer is queued, not yet completed. Poll GET /v1/transfers/:merchantTxRef until you get a terminal status:

async function waitForTransfer(merchantTxRef, maxAttempts = 10) {
for (let i = 0; i < maxAttempts; i++) {
const transfer = await kanall('GET', `/v1/transfers/${merchantTxRef}`)

if (transfer.status === 'successful') return transfer

if (transfer.status === 'failed' || transfer.status === 'reversed') {
throw new Error(`Transfer ${transfer.status}: ${transfer.narration}`)
}

await new Promise(r => setTimeout(r, 5000))
}

throw new Error('Transfer status unknown after max attempts')
}

The same polling loop applies in Python, Go, and Java — check status on each response and sleep 5 seconds between attempts.

Transfer statuses:

StatusMeaning
pendingQueued, not yet sent to the bank
processingSent to the bank, awaiting confirmation
successfulFunds delivered
failedTransfer failed — funds remain in the virtual account
reversedTransfer was reversed — funds returned to the virtual account

Calculate fee before settling

To know the exact gross amount Emeka needs to receive, use the fee calculator. This is useful when you want to top up a specific net amount.

GET /v1/fees/calculate?net=45000.00
const fees = await kanall('GET', '/v1/fees/calculate?net=45000.00')

Response:

{
"net": "45000.00",
"fee": "50.00",
"gross": "45050.00"
}

gross is the amount that must be in the virtual account balance. net is what the recipient receives. fee is Nomba's NIP charge.


Common errors

ErrorCauseFix
422 Unprocessable Entityamount sent as a number, not a stringSend "amount": "45000.00" not "amount": 45000
400 insufficient balanceVirtual account balance is lower than the transfer amountConfirm the ledger balance and check for pending entries
404 Not Found on bank lookupInvalid bankCodeUse GET /v1/transfers/banks to get valid codes
400 account name mismatchLookup returned a different name than expectedRe-verify the account number and bank code before settling

Congratulations. You have completed the full StarLine Gas integration:

  • Distributors get dedicated NUBANs at onboarding
  • Payments land in isolated ledgers — no matching required
  • Finance gets a daily confirmed collection report by route
  • Balances flow back to distributors via one API call

This same pattern applies to any platform that collects on behalf of multiple parties. The primitive does not change — only your business logic does.