Skip to content

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.

  1. Get a token.

    POST/api/auth/channel/token
    Terminal 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 for expires_in seconds. See Authentication.

  2. 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:create
    Terminal 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"
    }'

    Store txnAccountNumber: it is the merchant’s identity on every later call (X-Agent-Account) and also its agentCode. 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, or PARTNER_SUBAGENT_FORBIDDEN when your client may not onboard agents. See Onboard a sub-agent.

  3. 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. Omit beneficiary_account and payments settle to the merchant’s own transaction account.

    POST/api/legacy/agent/createFVAscope: virtual-accounts:create
    Terminal 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"
    }'

    desc is 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.

  4. The payer transfers. A customer of the merchant sends ₦50,000 from their own bank to 4000001050. NIBSS asks BankCore for the account name (your firstName 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 a NIP_INWARD transaction.

  5. Receive the notification and verify it. Within about ten seconds a transaction.completed webhook is posted to your subscription. If the merchant’s range is a remote-validated band, the same credit also triggers a callback to the notifyUrl on that range; see Inflow callbacks.

    POST /webhooks/bankcore HTTP/1.1
    X-Webhook-Id: 2c0e8f7a-1b3d-4e5f-9a6b-7c8d9e0f1a2b
    X-Webhook-Event: transaction.completed
    X-Webhook-Timestamp: 1788252312
    X-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 claim eventId in your store and answer 200. Match the payment to the merchant on virtualAccountNumber (or beneficiaryAccount), and credit the merchant with netAmount. The full procedure, with test vectors, is in Verifying signatures.

  6. 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:read
    Terminal 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" }

    desc is the account’s status (ACTIVE, USED, EXPIRED); a single-use account that has been paid reads USED. Two limits to know: the reference is 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 answers 0012 “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 same reference and nipSessionId. See Check a payment.

  7. Read the merchant’s balance.

    GET/api/legacy/agent/balancescope: accounts:read
    Terminal 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 }

    main is the available balance of the transaction account, income that of the fee account. Both are available balances, so a hold on the account is already deducted. See Balance and statement.

  8. 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_INWARD line should have a matching eventId claimed 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.

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

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.