Skip to main content

Step 2: Provision Distributor Accounts

Each distributor in StarLine's network gets their own dedicated NUBAN. You provision it once — the NUBAN is permanent and reused across all future invoices for that distributor.

Provision a single account

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

const account = await kanall('POST', '/v1/accounts', {
externalRef: 'distributor-emeka',
name: 'Emeka Okafor',
})
FieldValueWhy
externalRefdistributor-emekaYour internal distributor ID — Kanall uses this to link payments back to your record
nameEmeka OkaforDisplayed when Emeka looks up the account number at his bank

Payment notifications go to the tenant webhook URL you configured in Step 1. No callbackUrl needed per account unless you want to override it for a specific account.

Response:

{
"AccountRef": "distributor-emeka",
"BankAccountNumber": "0123456789",
"BankAccountName": "Emeka Okafor",
"BankName": "Nomba MFB",
"Currency": "NGN",
"Status": "active",
"CreatedAt": "2026-07-01T10:30:00Z"
}

BankAccountNumber is the NUBAN. Print it on Emeka's invoice. When he transfers to that number, Kanall knows it's his payment — no reference matching required.


Provision accounts in bulk

At onboarding, provision accounts for your full distributor list. Kanall enforces a rate limit of 20 provisioning requests per minute per API key — batch accordingly.

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

// Fetch distributors that don't yet have a NUBAN
const distributors = await db.query(
'SELECT id, name FROM distributors WHERE kanall_ref IS NULL'
)

for (const distributor of distributors.rows) {
try {
const account = await kanall('POST', '/v1/accounts', {
externalRef: `distributor-${distributor.id}`,
name: distributor.name,
})

await db.query(
'UPDATE distributors SET nuban = $1, kanall_ref = $2 WHERE id = $3',
[account.BankAccountNumber, account.AccountRef, distributor.id]
)

console.log(`Provisioned ${distributor.name}: ${account.BankAccountNumber}`)
} catch (err) {
console.error(`Failed for ${distributor.name}: ${err.message}`)
}

// Stay within the 20 req/min rate limit
await new Promise(r => setTimeout(r, 3100))
}

Store what you need

After provisioning, your distributors table should hold:

ColumnValuePurpose
nuban0123456789Print on invoices, share with the distributor
kanall_refdistributor-emekaUsed to look up the ledger via GET /v1/accounts/distributor-emeka/statement

Add the distributor's NUBAN to every invoice you send:

═══════════════════════════════════════════
StarLine Gas — Invoice
INV-2026-007 │ Due: 2026-07-10
Distributor: Emeka Okafor (Route 7 — Ikeja)
Amount due: ₦45,000.00

PAY TO:
Bank: Nomba MFB
Account: 0123456789
Name: Emeka Okafor

Transfer exactly ₦45,000.00 to this account.
═══════════════════════════════════════════

When Emeka transfers to that NUBAN, Kanall receives the event and fires your webhook within seconds.


Common errors

ErrorCauseFix
401 UnauthorizedMissing or invalid X-API-KeyCheck your API key is set in env and not expired
409 ConflictexternalRef already exists under this tenantUse a unique ref per entity, or fetch the existing account instead
422 Unprocessable EntityMissing required fieldInclude both externalRef and name in the request body
429 Too Many RequestsRate limit exceeded (20 provisioning req/min)Add a delay between requests in your bulk loop

Next: Receive payment webhooks →