Idempotency
Networks fail at the worst moment: after the bank has posted your debit and before you have read the response. Idempotency is how you recover from that without posting it twice. The API gives you one mechanism, a reference you choose, and one guarantee: the same reference will not be posted twice for the same kind of transaction.
How duplicates are detected
Section titled “How duplicates are detected”Every transaction BankCore creates can carry an external reference, the value you supplied. Uniqueness is enforced per bank tenant on the pair (external reference, transaction type), twice over: the service looks the pair up before posting, and the database has a unique index on it as the backstop against two concurrent calls. A collision at either layer is answered with legacy code 0094.
“Transaction type” is BankCore’s internal classification, and the money endpoints you call map onto it as follows:
| Your call | Type checked | Notes |
|---|---|---|
| Fund, charge, deposit, transfer between accounts at the bank | Internal transfer (TRANSFER_OUT) |
All four share one type, so a reference used on any of them cannot be reused on another |
| Interbank transfer (NIP) | Checked against earlier NIP transfers | A reused reference is refused before the debit |
| Bill payment, airtime | Bill payment / airtime | See the note below |
| Virtual account creation | The virtual account’s external_ref |
Unique per tenant among virtual accounts, independently of transactions |
Because the pair includes the type, a reference used for a fund could in principle be reused for an interbank transfer. Do not rely on it. Generate references that are globally unique across everything you send (a UUID, or a prefix plus a database sequence) and the question never arises.
If you omit the reference, no duplicate check is possible and every call posts. Always send one on money movement.
Which field carries the reference
Section titled “Which field carries the reference”| Endpoint family | Field | Where it comes back |
|---|---|---|
/wallets/transactions/fund, /charge, /transfer, /banktransfer, /bill/pay, /airtime, /{account}/deposit |
transactionRef in the JSON body |
clientRef on the response |
Agent /transact/transfer/direct_debit, /transact/paybill, /transact/airtime |
transactionRef |
clientRef |
Agent /virtual_account/create |
external_ref |
Used again on /virtual_account/check/{account}/{reference} and stamped on the inflow’s webhook |
Provider check-transaction (/wallets/transactions/checktran) |
transactionRef |
Looks your reference up first, then BankCore’s retRef |
Agent check-transaction (/transact/checktran) |
transactionRef |
Matches BankCore’s retRef only. Your own reference is not found here; keep the retRef from the original response |
transactionRef is a free string of at most 100 characters. Keep it to printable ASCII with no leading or trailing whitespace. It is echoed back to you verbatim and shown to bank staff, so make it meaningful.
What 0094 means
Section titled “What 0094 means”{ "code": "0094", "desc": "Duplicate transaction reference: SAL-20260401-001"}The reference has been seen before for this transaction type. It does not tell you whether the earlier attempt succeeded, is pending, or failed: a failed interbank transfer still occupies its reference. Look it up before deciding anything.
Safe retry rules
Section titled “Safe retry rules”- Generate the reference before you send, and persist it first. If your process dies between generating and sending, you must be able to find the reference again on restart.
- On a transport failure, retry with the same reference. A timeout, a reset connection or a 5xx from the edge means you do not know what happened. Resend the identical request. If the first attempt posted, you get
0094and go to rule 4; if it did not, the retry posts normally. - Never mint a new reference for a retry. A new reference is a new transaction. This is the single most common way partners double-pay.
- On
0094, look the reference up and act on the status you find. On the provider money endpoints call check-transaction with the sametransactionRef; on the agent endpoints read the statement or ask bank support, since the agent check-transaction does not search by your reference. Do not treat0094as success or as failure. - Treat
0009as pending, not as failure. The transaction exists and money may have moved. Poll Check a transaction with backoff until it returns0000(completed) or0096(failed or reversed). Never resend a transaction that returned0009. - 403 and 429 are free. A scope, allowlist or rate-limit refusal happens before anything is posted. Retry with the same reference once the cause is fixed.
- A corrected request is a new transaction. If you change the amount, the accounts or the beneficiary, use a new reference. Reusing the old one is refused anyway.
Check-transaction as the source of truth
Section titled “Check-transaction as the source of truth”The two check-transaction endpoints behave differently, and the difference matters for recovery:
- Provider endpoint (
POST /api/legacy/provider/wallets/transactions/checktran) accepts yourtransactionRef, looks for it first and falls back to BankCore’sretRef. Where a transfer has fee, VAT or stamp-duty legs sharing your reference it answers with the principal leg. The code follows the requery convention:0000completed,0009pending or held,0096failed or reversed,0012not found. The response also carriesretRef,clientRef, the accounts andtransdatetime. - Agent endpoint (
POST /api/legacy/agent/transact/checktran) matches BankCore’sretRefonly. When found it answers0000whatever the state, with the status name (COMPLETED,PENDING,FAILED,REVERSED) indesc;0012means not found. Branch ondeschere, and only here.
See Check a transaction.
For a transfer made through a provider money endpoint, a 0012 from the provider check-transaction after a transport failure is the one case where resending is the right answer: the bank has no record of your reference, so the first attempt never arrived. For a transfer made through an agent endpoint you only have a retRef once a response reached you, so after a transport failure resend with the same transactionRef and let 0094 tell you whether it had already posted.
In the portal
Section titled “In the portal”The Transactions page lists every transaction with its reference and status and can be searched by reference, which is how an operator confirms whether a disputed transaction ever posted.