Walkthrough: merchant to first inflow
This page strings the other sections together for the commonest partner integration: you are an aggregator, you sign up merchants, each merchant gets a permanent account number its customers can pay into, and you want to know the moment money lands. Every call below is documented in full elsewhere; the point here is the order and the handoffs.
You need a partner client holding sub-agents:create, virtual-accounts:create, virtual-accounts:read, accounts:read and transactions:read, with sub_agent_access_enabled switched on by the bank, and a webhook subscription on transaction.completed. The merchant will need a virtual account range of its own; ask for that when you ask for the client.
-
Get a token.
POST/api/auth/channel/tokenTerminal window curl -X POST https://api.bankcore.ng/api/auth/channel/token \-H "Content-Type: application/json" \-d '{"client_id":"pa_ab12cd34","client_secret":"sk_live_xxxxxxxxxxxxxxxxxxxx"}'Keep the
access_token; it is good forexpires_inseconds. See Authentication. -
Onboard the merchant. This is the one agent route that takes no
X-Agent-Account: you are creating the agent, not acting for one. Verify the owner’s BVN or NIN first; the date of birth that comes back from that verification is required.POST/api/legacy/agent/admin/add_agentscope: sub-agents:createTerminal window curl -X POST https://api.bankcore.ng/api/legacy/agent/admin/add_agent \-H "Authorization: Bearer $TOKEN" \-H "Content-Type: application/json" \-d '{"firstName": "Adeola","lastName": "Martins","phoneNumber": "08012345678","email": "adeola@example.com","bvn": "22222222222","dateOfBirth": "1990-05-15","gender": "F","address": "12 Marina Road","state": "Lagos","localgovt": "Lagos Island","agentCategory": "MERCHANT","businessType": "REGISTERED_BUSINESS","businessName": "Adeola Stores","cacRegistrationNumber": "BN1234567"}'{"customerId": "e41cc8ae-f521-4859-91da-d489f091a9b9","customerNumber": "C000182736","agentProfileId": "5a6b7c8d-9e0f-4a1b-8c2d-3e4f5a6b7c8d","agentCode": "6020012345","txnAccountNumber": "6020012345","feeAccountNumber": "6020012346","agentCategory": "MERCHANT","businessType": "REGISTERED_BUSINESS","displayName": "Adeola Stores"}Store
txnAccountNumber: it is the merchant’s identity on every later call (X-Agent-Account) and also itsagentCode. The merchant is stamped as created by your client, which is what makes its accounts reachable to you from now on.This route is not legacy-shaped. A refusal is HTTP 400 with
{ timestamp, errorCode, message, path, status }:DATE_OF_BIRTH_REQUIRED,BVN_NIN_REQUIRED,BUSINESS_NAME_REQUIRED,DUPLICATE_PHONE,DUPLICATE_BVN,DUPLICATE_NIN, orPARTNER_SUBAGENT_FORBIDDENwhen your client may not onboard agents. See Onboard a sub-agent. -
Create a non-expiring virtual account for the merchant. Act as the merchant with
X-Agent-Account. The number this returns is a virtual account that never expires and accepts any number of payments, which is what a merchant prints on an invoice or a shop sign. Omitbeneficiary_accountand payments settle to the merchant’s own transaction account.POST/api/legacy/agent/createFVAscope: virtual-accounts:createTerminal window curl -X POST https://api.bankcore.ng/api/legacy/agent/createFVA \-H "Authorization: Bearer $TOKEN" \-H "X-Agent-Account: 6020012345" \-H "Content-Type: application/json" \-d '{"firstName": "Adeola","lastName": "Stores","phoneNumber": "08012345678"}'{ "code": "0000", "desc": "4000001050" }descis the virtual account number. Give it to the merchant: it never expires and accepts any number of payments, each settling to the merchant’s transaction account. See Create a non-expiring virtual account. -
The payer transfers. A customer of the merchant sends ₦50,000 from their own bank to
4000001050. NIBSS asks BankCore for the account name (yourfirstName lastName), the payer confirms, and the credit arrives. BankCore takes the range’s fee and posts the net amount to the merchant’s transaction account as aNIP_INWARDtransaction. -
Receive the notification and verify it. Within about ten seconds a
transaction.completedwebhook is posted to your subscription. If the merchant’s range is a remote-validated band, the same credit also triggers a callback to thenotifyUrlon that range; see Inflow callbacks.POST /webhooks/bankcore HTTP/1.1X-Webhook-Id: 2c0e8f7a-1b3d-4e5f-9a6b-7c8d9e0f1a2bX-Webhook-Event: transaction.completedX-Webhook-Timestamp: 1788252312X-Webhook-Signature: sha256=...{"type":"transaction.completed","eventId":"2c0e8f7a-1b3d-4e5f-9a6b-7c8d9e0f1a2b","reference":"NIN20260901091533ABCDEF","txType":"NIP_INWARD","amount":48500.00,"currency":"NGN","destinationAccountNumber":"6020012345","accountNumber":"6020012345","accountName":"ADEOLA STORES","customerType":"AGENT","senderName":"JOHN DOE","senderAccount":"0123456789","senderBank":"000014","senderBankName":"Access Bank","nipSessionId":"000014260901091533000000000001","narration":"Payment for order 1234","virtualAccountNumber":"4000001050","beneficiaryAccount":"6020012345","beneficiaryName":"ADEOLA STORES","grossAmount":50000.00,"feeAmount":1500.00,"netAmount":48500.00}Recompute
HMAC-SHA256(secret, "1788252312." + raw_body), compare in constant time, check the timestamp is within your window, then claimeventIdin your store and answer200. Match the payment to the merchant onvirtualAccountNumber(orbeneficiaryAccount), and credit the merchant withnetAmount. The full procedure, with test vectors, is in Verifying signatures. -
Check the payment. The check route confirms a dynamic virtual account’s state by number and your reference:
GET/api/legacy/agent/virtual_account/check/{accountNumber}/{reference}scope: virtual-accounts:readTerminal window curl https://api.bankcore.ng/api/legacy/agent/virtual_account/check/4000001042/ORDER-1234 \-H "Authorization: Bearer $TOKEN" \-H "X-Agent-Account: 6020012345"{ "code": "0000", "desc": "USED" }descis the account’s status (ACTIVE,USED,EXPIRED); a single-use account that has been paid readsUSED. Two limits to know: thereferenceis accepted but not used to look anything up, and the route only knows dynamic virtual accounts, so for the non-expiring account created in step 3 it answers0012“Virtual account not found”. For a non-expiring account the proof of payment is the webhook and the merchant’s statement (GET /api/legacy/agent/statement,transactions:read), where the credit appears with the samereferenceandnipSessionId. See Check a payment. -
Read the merchant’s balance.
GET/api/legacy/agent/balancescope: accounts:readTerminal window curl https://api.bankcore.ng/api/legacy/agent/balance \-H "Authorization: Bearer $TOKEN" \-H "X-Agent-Account: 6020012345"{ "main": 48500.0000, "income": 0.0000 }mainis the available balance of the transaction account,incomethat of the fee account. Both are available balances, so a hold on the account is already deducted. See Balance and statement. -
Reconcile. At the end of the day, list the merchant’s statement for the date range and compare it with the webhooks you stored: every
NIP_INWARDline should have a matchingeventIdclaimed on your side, and every claimed event a statement line. Anything missing on your side is a delivery that failed; anything missing on ours is a payment that never arrived, whatever the payer says. Reconciling a day describes the full procedure.
What can go wrong
Section titled “What can go wrong”| Symptom | Likely cause | Where to look |
|---|---|---|
add_agent answers 400 PARTNER_SUBAGENT_FORBIDDEN |
Your client lacks sub-agents:create, or sub-agent access is not enabled |
Ownership errors |
createFVA answers 0012 naming a range |
The merchant has no allocatable range | Ask the bank for one |
| The payer’s bank says “account name not found” | The number is not yet active, or the range is remote-validated and your validation endpoint refused | Remote validation contract |
| No webhook, but the money is on the statement | The merchant’s account was not in your scope when the event fired, or the delivery failed and your endpoint answered 4xx |
Webhook endpoints in the portal, then Delivery and retries |
| Webhook arrives, signature fails | Secret used as hex bytes, body re-serialised, or clock skew | Verifying signatures |
In the portal
Section titled “In the portal”Agents lists the merchants you onboarded; Virtual account numbers lists their non-expiring and dynamic accounts; Payments received lists every inflow with whether your systems were told; Webhook endpoints shows the deliveries behind that answer.