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.