> ## Documentation Index
> Fetch the complete documentation index at: https://www.bitnob.dev/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> Bitnob API requests authenticate with an `X-Auth-Client` API key header against https://api.bitnob.com.
> Always use the sandbox environment for testing. Never put live API keys in sample code.
> Money amounts are integers in the smallest unit of the currency (satoshis for BTC, cents for fiat). Never use floats.
> Every state-changing request takes a client-supplied `reference` and must be treated as idempotent on retry.
> Quotes expire. Re-quote rather than reusing a stale quote id.

# Idempotency and references

> References, deduplication on both sides, and safe retries, so that no request moves money twice.

A reference is a unique string you attach to an action that moves money. It is your idempotency key. If the same request reaches us twice with the same reference, we execute it once.

Retries are normal. Networks time out, workers restart, and users press Send twice. Without a reference, each of those repeats a payout. With one, each repeat is harmless.

This guide covers how to design references and how to deduplicate on your side.

## Where duplicates come from

Four causes produce almost every duplicate:

| Cause | What happens |
| - | - |
| Request timeout | Your call times out after we accepted it. You retry, not knowing the first attempt succeeded. |
| Webhook retry | Your endpoint fails or is slow, so we send the event again. |
| User action | The user submits a form twice, or reloads mid-request. |
| Queue redelivery | A worker crashes after processing and before acknowledging, so the job runs again. |

None of these are faults in your code. They are properties of distributed systems, and the reference is the defense against all four.

## Design a reference

Make references unique, stable, and readable.

```json theme={null}
{
  "reference": "payout_20260408_a41f9c"
}
```

Follow four rules:

1. Generate the reference **before** the first attempt, and store it with the intent.
2. Include a type prefix, such as `payout_`, `trade_`, or `card_`.
3. Include enough entropy to avoid collision, such as a date and a short random suffix.
4. Reuse the same reference for every retry of the same business action.

> [!WARNING]
>
> Never generate a new reference on retry. A new reference is a new action, and it will execute a second time.

On card calls, a reference can contain only letters, digits, hyphens, and underscores.

A bare timestamp such as `1689731218712` or a bare UUID satisfies uniqueness but tells you nothing in a log. A readable prefix pays for itself during an incident.

## Deduplicate on your side

We enforce the reference. You still need your own check, because your system also processes each event.

Record the key before you act, and check it before you act again. For a webhook, the key is `event_id`. Several events can share one reference, so don't use `reference` there. For your own requests and jobs, the key is the reference.

```javascript theme={null}
if (await db.isProcessed(event.event_id)) {
  return res.status(200).send("already processed");
}

await processEvent(event);
await db.markAsProcessed(event.event_id);
res.status(200).send("ok");
```

Put this check in three places:

* Your webhook handler, using `event_id`.
* Your user-facing send or payment endpoint, using the reference.
* Your asynchronous job processors, using the reference.

### Store references in a dedicated table

| id | reference | status | source | created\_at |
| - | - | - | - | - |
| 1 | `payout_20260408_a41f9c` | processed | api | 2026-04-08 12:22:03 |
| 2 | `trade_20260408_77b210` | failed | job-worker | 2026-04-08 12:23:10 |

A dedicated table gives you a fast uniqueness check, an audit trail, and the data behind a replay tool.

### Lock across concurrent workers

A check followed by a write is not atomic. If two workers run the check at the same moment, both pass it.

Take a lock, either in your database:

```sql theme={null}
BEGIN;

SELECT * FROM idempotency_keys
WHERE reference = 'payout_20260408_a41f9c'
FOR UPDATE;

-- Insert the row if it does not exist, then process.

COMMIT;
```

Or in Redis:

```redis theme={null}
SET processed:payout_20260408_a41f9c true NX EX 86400
```

`SET ... NX` succeeds only if the key is absent. Treat a failed `SET` as "another worker owns this reference".

## Where references appear

Send a reference on every endpoint that moves value:

* [Initialize payout](https://www.bitnob.dev/api-reference/payouts/initialize-payout.md)
* [Create withdrawal](https://www.bitnob.dev/api-reference/withdrawals/initiate-withdrawal.md)
* [Submit order](https://www.bitnob.dev/api-reference/trading/submit-order.md)
* [Fund or withdraw card balance](https://www.bitnob.dev/api-reference/cards/fund-or-withdraw-balance.md)

The reference comes back on the object and on the webhook event. That is what lets you match our record to yours.

## Handle a duplicate response

What a reused reference does depends on the endpoint:

| Endpoint | Reused reference |
| - | - |
| Payouts | Returns the existing payout instead of creating a second one. |
| Withdrawals | Rejected with `409` `DUPLICATE_KEY_ERROR`. Nothing is sent. Look the original up with [Get transaction](https://www.bitnob.dev/api-reference/transactions/get-transaction-by-id.md). |
| Trading, NGN pairs | Returns the original order with the same `id`. Nothing executes twice. |
| Trading, crypto pairs | **Not checked.** A second request with the same reference can execute a second trade. Look the order up with `GET /api/trading/orders/{reference}` before you resend. |
| Virtual accounts | Returns the existing account. |
| Create card | Rejected with `409` `ALREADY_EXISTS`. No second card is created. |
| Card funding and withdrawals | Returns the original transaction if it completed, unless the card's cooldown refuses the call first. Returns an error if it's still pending or failed. See [Fund a card](https://www.bitnob.dev/docs/card-issuing/card-funding.md). |

In every case, generate the reference before the first attempt and never mint a new one for a retry. A UUID is fine, as long as you store it against the transaction before the first attempt. See [Errors and retries](https://www.bitnob.dev/docs/getting-started/api-best-practices/errors-and-retries.md).

## Worked example

A user requests a payout. Your service does the following:

**1. Create and store the reference**

Generate `payout_20260408_a41f9c` and write it to your database with status `pending`.

**2. Submit the payout**

Call [Initialize payout](https://www.bitnob.dev/api-reference/payouts/initialize-payout.md) with that reference. The call times out.

**3. Retry with the same reference**

Submit the identical request. We recognize the reference and return the original result. Nothing executes twice.

**4. Receive the webhook**

We send `payouts.withdrawal.success` carrying the same reference and a new `event_id`. Your handler checks the `event_id`, finds it unprocessed, and marks the payout delivered.

**5. Receive the webhook again**

A retried delivery arrives with the same `event_id`. Your handler finds it already processed and returns `200` without acting.

## Avoid these patterns

**Not storing the reference.** If you cannot search your logs by reference, you cannot answer a support question, and neither can we.

**Generating references inside a retry loop.** Each attempt then becomes a separate payout.

**Treating every request as new.** Any system that assumes each call is unique will duplicate money movement under retry.

## Summary

| Practice | What it gives you |
| - | - |
| One reference per business action | Exactly-once execution |
| Reference generated before the first attempt | Safe retry after a timeout |
| Reference stored and indexed | Traceability and support visibility |
| Lock around the check and the write | Safety across concurrent workers |
| `event_id` check in the webhook handler | Safety against redelivery |

## Next

Continue to [Webhooks and event handling](https://www.bitnob.dev/docs/getting-started/api-best-practices/webhook-best-practices.md), which covers how to receive and verify the events that carry your references back.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.