Skip to content

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.

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.

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.

{
"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.

  1. 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.
  2. 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 0094 and go to rule 4; if it did not, the retry posts normally.
  3. 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.
  4. On 0094, look the reference up and act on the status you find. On the provider money endpoints call check-transaction with the same transactionRef; on the agent endpoints read the statement or ask bank support, since the agent check-transaction does not search by your reference. Do not treat 0094 as success or as failure.
  5. Treat 0009 as pending, not as failure. The transaction exists and money may have moved. Poll Check a transaction with backoff until it returns 0000 (completed) or 0096 (failed or reversed). Never resend a transaction that returned 0009.
  6. 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.
  7. 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.

The two check-transaction endpoints behave differently, and the difference matters for recovery:

  • Provider endpoint (POST /api/legacy/provider/wallets/transactions/checktran) accepts your transactionRef, looks for it first and falls back to BankCore’s retRef. 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: 0000 completed, 0009 pending or held, 0096 failed or reversed, 0012 not found. The response also carries retRef, clientRef, the accounts and transdatetime.
  • Agent endpoint (POST /api/legacy/agent/transact/checktran) matches BankCore’s retRef only. When found it answers 0000 whatever the state, with the status name (COMPLETED, PENDING, FAILED, REVERSED) in desc; 0012 means not found. Branch on desc here, 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.

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.