Responses and error codes
The API has two response families, and you must know which one an endpoint belongs to before you can read its answer. The rule is by path: /api/legacy/** endpoints use the legacy envelope; everything else is BankCore-native. The money-movement, agent and virtual-account endpoints in this documentation are almost all legacy.
The legacy envelope
Section titled “The legacy envelope”Legacy endpoints answer HTTP 200 whether the call succeeded or failed. The outcome is in the body:
{ "code": "0000", "desc": "Successful"}code is a four-digit string and desc a human-readable description safe to log or show. Null fields are omitted from the JSON entirely, so parse defensively: a field that is absent is simply not applicable to that call.
Money-movement calls return an enriched envelope. A successful fund, charge or transfer looks like:
{ "code": "0000", "desc": "Successful", "retRef": "TXN20260401143000001", "clientRef": "SAL-20260401-001", "fromAccount": "6020000001", "toAccount": "6020012345", "availableBalance": "145000.00", "ledgerBalance": "150000.00", "transdatetime": "20260401143000"}Every field the envelope can carry:
| Field | Appears on | Meaning |
|---|---|---|
code, desc |
Every legacy response | Outcome and description |
retRef |
Money movement, check-transaction | BankCore’s own reference for the transaction. Quote it to the bank |
clientRef |
Money movement, check-transaction | Your transactionRef, echoed back so you can match the response to your request |
fromAccount, toAccount |
Money movement | The accounts debited and credited |
availableBalance, ledgerBalance |
Money movement | Balances of the debited account after posting, as strings with two decimals. Available is ledger minus holds |
transdatetime |
Money movement | Posting time in yyyyMMddHHmmss; see Environments for the zone caveat |
sessionId |
Interbank transfer | The NIP session id for the transfer |
kycTier, accountDesignation, idType, nameEnquirySessionId, bvn |
Name enquiry | Beneficiary attributes returned by the enquiry; echo them on the transfer call that follows |
token, units, receipt, providerRef, subscriberAddress |
Bill payment | Prepaid token and units vended, the biller’s receipt number, the provider’s reference, and the service address |
Legacy list endpoints (statements, searches, downlines, the biller catalogue) do not use the envelope. They answer:
{ "success": true, "total": 142, "data": [ { "...": "..." } ]}with total the number of matching rows across all pages, and pagination 1-based (page=1 is the first page).
BankCore-native responses
Section titled “BankCore-native responses”Native endpoints (the token endpoint, KYC documents, device registration, tier upgrades) use real HTTP statuses. A success is a 2xx with a resource body. A failure is a 4xx or 5xx with:
{ "timestamp": "2026-04-01T20:57:11.298Z", "errorCode": "ENTITY_NOT_FOUND", "message": "Account not found: 6020099999", "path": "/api/kyc-documents/by-account/6020099999", "status": 404}| Field | Meaning |
|---|---|
timestamp |
When the error occurred, ISO-8601 UTC |
errorCode |
A stable, upper-snake-case code to branch on. Business errors carry their own code (INSUFFICIENT_FUNDS, PARTNER_ACCOUNT_FORBIDDEN); framework errors use VALIDATION_ERROR, MALFORMED_REQUEST, INVALID_PARAMETER, ENTITY_NOT_FOUND, UNAUTHORIZED, ACCESS_DENIED, INTERNAL_ERROR |
message |
Human-readable, safe to log |
validationErrors |
Only on VALIDATION_ERROR: a map of field name to problem |
path |
The request path |
status |
The HTTP status, repeated |
Native list endpoints are 0-based.
Legacy code table
Section titled “Legacy code table”| Code | Meaning | When you will see it |
|---|---|---|
0000 |
Success | The operation completed. For money movement, the posting is final |
0004 |
Insufficient funds | The debited account (yours, an agent’s, or your settlement account on a fund) cannot cover amount plus fees |
0009 |
Pending / in flight | The operation was accepted and is awaiting an external party: an interbank transfer not yet confirmed by NIBSS, a bill payment awaiting the provider, a POS authorisation awaiting the acquirer. Money may already have moved. Do not retry; poll Check a transaction with the retRef you were given until it settles to 0000 or 0096 (the agent check-transaction reports the state in desc instead) |
0012 |
Invalid / not found | A validation failure (missing field, bad format, missing X-Agent-Account), an entity that does not exist, a velocity or amount limit, a watchlisted BVN, or an ownership refusal (PARTNER_ACCOUNT_FORBIDDEN, PARTNER_SUBAGENT_FORBIDDEN). desc says which |
0014 |
Duplicate record | An agent with this BVN, NIN or phone number already exists |
0062 |
Account not active | The account is frozen, locked, suspended, closed or under a post-no-debit restriction |
0094 |
Duplicate reference | Your transactionRef has already been used for this kind of transaction. See Idempotency |
0096 |
System error / failed | Either the operation failed in a terminal way (an interbank transfer rejected or reversed, a bill payment the provider refused, with funds restored) or something unexpected happened. Read desc, then check the transaction before doing anything else |
How to branch on a response
Section titled “How to branch on a response”For a legacy money-movement call:
- Transport failure (timeout, connection reset, HTTP 5xx from the edge): you do not know what happened. On a provider money endpoint, call check-transaction with your
transactionRef; if it is not found, retry the original call with the same reference. On an agent endpoint simply retry with the same reference and let0094tell you whether it had already posted (the agent check-transaction searches only BankCore’sretRef). - HTTP 401: refresh the token and retry once.
- HTTP 403 or 429: nothing happened. Fix the cause (scope, address) or wait, then retry with the same reference.
- HTTP 200,
code0000: done. StoreretRef. 0009: accepted, not final. StoreretRef, poll check-transaction with backoff, and treat the money as moved until told otherwise.0094: you already sent this. Look the reference up with check-transaction rather than assuming either outcome.0004,0012,0014,0062: refused before any posting. Safe to correct and resend, with a new reference if the amount or parties changed.0096: readdesc. If it describes a terminal failure of a known transaction (reversed, rejected), you are done and funds are restored. If it is a generic system error, check the transaction before retrying.
Never branch on desc text; it is written for people and may change. Branch on code, and on errorCode for native endpoints.
In the portal
Section titled “In the portal”The Transactions page shows every transaction your client created with its reference, status and posting time, so an operator can confirm what the API told you.