Contents

Alex funds his wallet with $200. Nothing happens for a few seconds, so he tries again, and this time the payment goes through. A minute later his balance reads $400 instead of $200.

What happened? Alex's first tap reached your API and was recorded, but the response timed out before it got back to him. When he tapped again, your API had no memory of the first call, so it wrote a second entry.

Two transactions, one payment, and your funding account is now missing $200.

That's the problem idempotency solves. A transaction request is idempotent when repeating it produces the same result, not a second entry. The first request records the transaction and updates the balance. Every repeat after that is ignored. It's what makes retries safe: when your app can't tell whether a request landed, it can send the same request again without worrying about doubling the transaction.

How Blnk handles idempotency

Blnk handles idempotency through a required reference field on every transaction. Only the first transaction with a given reference is recorded. Every duplicate after that is discarded automatically.

You don't need a separate header, an extra table, or a cache to invalidate. The reference you already send with every transaction is the idempotency key.

Here's what a transaction request in Blnk looks like:

```

--CODE language-bash--

curl -X POST "http://YOUR_BLNK_INSTANCE_URL/transactions" \
 -H "X-blnk-key: YOUR_API_KEY" \
 -H "Content-Type: application/json" \
 -d '{
   "amount": 200,
   "reference": "topup_alex_2026_04_17_001",
   "currency": "USD",
   "precision": 100,
   "source": "@WorldUSD",
   "destination": "bln_alex_wallet",
   "description": "Wallet top-up",
   "allow_overdraft": true
 }'

```

If the first request succeeds, Blnk records the transaction and returns it. If your app retries with the same reference, Blnk discards the duplicate silently.

Alex's balance only moves once, and your ledger stays correct.

Discarded vs. rejected

It's worth being precise here, because this matters when you're debugging.

Blnk has two different outcomes for a transaction that doesn't get applied:

  • Rejected transactions are recorded in the ledger. These are transactions Blnk received, validated, and decided not to apply. For example, if Alex tries to send $500 from a wallet that only holds $300, Blnk saves it with status: "REJECTED" and a reason in meta_data, so you still have a trace of the attempt.
  • Discarded transactions, however, are not recorded. This is what happens when Blnk detects a duplicate reference in your transaction request.

So when a duplicate comes in, you won't see a second row piling up next to your original. You'll just see the one original transaction, exactly as it was recorded.

The ledger looks the same whether your app retried once, twice, or ten times.

Best practices for idempotency in Blnk

Because the reference does the work of an idempotency key, how you generate it matters. Here are a few things to keep in mind:

  1. One reference per money movement. If Alex retries his $200 top-up three times, all three requests should carry the same reference. The reference belongs to the operation, not to each attempt.
  2. Create the reference before any retry logic runs. Build it before you enter the retry loop. If you create it inside the loop, every attempt gets a fresh reference, and Blnk has no way to know they're the same operation.
  3. Use meaningful identifiers when you can. A random UUID works, but a business identifier like an order_id or payout_id connected to an external payment processor / source is better. It ties the ledger record back to the domain object that caused it.

Code sample: retries done right

Let's go back to Alex's $200 top-up and walk through what can go wrong with retries, and how to fix it.

The broken version: your app generates a new reference on every attempt.

```

--CODE language-javascript--

async function topUpWallet(user, amount) {
 const maxRetries = 3

 for (let attempt = 0; attempt < maxRetries; attempt++) {
   try {
     const result = await blnk.transactions.create({
       reference: crypto.randomUUID(),
       amount,
       currency: "USD",
       source: "@WorldUSD",
       destination: user.walletId,
     })
     return result
   } catch (err) {
     if (attempt === maxRetries - 1) throw err
   }
 }
}

```

Here's what happens:

  • Attempt 1 hits Blnk and gets recorded, but the response never makes it back to your app.
  • Your app assumes the request failed and retries.
  • Attempt 2 generates a brand new reference, so Blnk treats it as a completely different transaction.
  • Alex's wallet gets credited $200 twice. Your funding account is now short $200.

The fixed version: generate the reference once, before any attempt.

```

--CODE language-javascript--

async function topUpWallet(user, order, amount) {
 const reference = `topup_${user.id}_${order.id}`
 const maxRetries = 3

 for (let attempt = 0; attempt < maxRetries; attempt++) {
   try {
     const result = await blnk.transactions.create({
       reference,
       amount,
       currency: "USD",
       source: "@WorldUSD",
       destination: user.walletId,
     })
     return result
   } catch (err) {
     if (attempt === maxRetries - 1) throw err
   }
 }
}

```

Now every retry carries the same reference. If Blnk already recorded the first attempt, the retry is discarded. If it didn't, the retry goes through.

Either way, Alex's wallet moves exactly once.

Beyond single transactions

The same reference rule extends to more complex flows.

  1. Bulk transactions. Each transaction in a batch needs its own unique reference. If you retry the whole batch after a timeout, already-recorded transactions are discarded and only the ones that didn't land are applied.
  2. Inflight transactions. The reference protects you from duplicate holds. Committing or voiding is done by transaction_id, so those calls are naturally idempotent: committing an already-committed transaction doesn't move balances a second time.
  3. Refunds. A refund is a new transaction, so it needs its own reference. A clean pattern: refund_{original_reference}. Unique, easy to reason about, and keeps the refund discoverable from the transaction it reverses.

In every case, the contract is the same: one reference, one recorded transaction. As long as your app generates references correctly and reuses them on retry, duplicates can't reach your ledger in Blnk.

Balances stay accurate, reconciliation stays honest, and Alex's $200 top-up stays a $200 top-up, no matter how many times the network hiccups.