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

# Idempotency

> How to make a retry safe, and why a blank key is rejected.

The dangerous moment in any payment integration is a request you sent but never got an answer to.
The charge may have committed. Retrying blindly charges the customer twice; giving up may hand
over goods that were never paid for.

An idempotency key removes the guess.

```http theme={null}
Idempotency-Key: order-84271-attempt-1
```

Send the same key on every retry of the same sale, and Vibepay collapses them into exactly one
payment. The first request charges; every repeat gets the original transaction back.

## The rule that matters

<Warning>
  **One key per sale attempt — reused across every retry of that attempt.** A key generated fresh
  on each HTTP call protects nothing: the retry carries a different key and becomes a second,
  separate charge.
</Warning>

Generate the key when the cashier confirms the amount, hold it for the whole attempt, and throw it
away only when you get a final answer.

```javascript theme={null}
// Right — generated once, per sale attempt.
const idempotencyKey = `sale-${saleId}-${Date.now()}`;

for (let attempt = 0; attempt < 3; attempt++) {
  try {
    return await charge(qrToken, amountMNT, idempotencyKey);  // same key each time
  } catch (e) {
    if (!isRetryable(e)) throw e;
    await backoff(attempt);
  }
}
```

```javascript theme={null}
// Wrong — a new key per call turns three retries into three charges.
for (let attempt = 0; attempt < 3; attempt++) {
  await charge(qrToken, amountMNT, crypto.randomUUID());
}
```

## What a repeat returns

<CardGroup cols={2}>
  <Card title="201 — same transaction" icon="circle-check">
    The repeat matched the original in every respect. You get the **original** transaction back,
    with the same `id`. The customer was charged once.
  </Card>

  <Card title="409 — conflict" icon="circle-exclamation">
    The key was reused for a *different* charge, or the original has since been reversed. Nothing
    was charged. Use a fresh key for a genuinely new sale.
  </Card>
</CardGroup>

Because a matched replay returns `201`, your success path needs no special handling at all — the
retry simply succeeds. That is the point.

## Choosing keys

| Rule       | Detail                                                                                                                 |
| ---------- | ---------------------------------------------------------------------------------------------------------------------- |
| Length     | 1–255 characters                                                                                                       |
| Uniqueness | Per merchant, not per terminal. Two tills in one shop share a namespace, so include something till-specific or random. |
| Format     | Anything. A UUID, or `sale-{id}-{timestamp}`, or your own order number plus an attempt nonce.                          |
| Reuse      | Never across different sales. A repeated key on a different amount is a `409`, not a charge.                           |

Since keys are scoped to your merchant rather than to a terminal, a bare sequential counter per
till will eventually collide with the till next to it. Prefix it, or use a UUID.

## Never send a blank key

```json 400 Bad Request theme={null}
Idempotency-Key must not be blank (omit the header entirely to opt out of retry collapsing)
```

An empty header is rejected on purpose. It reads, in code and in logs, as though idempotency is
implemented — while doing nothing at all. That is precisely the failure that double-charges a
customer on a timeout, so the API refuses it at the boundary instead of letting it through.

If you genuinely do not want retry collapsing, **omit the header**. That is allowed, and it is
explicit.

## What is protecting the customer

Three independent mechanisms, so a gap in one is covered by another:

<Steps>
  <Step title="The idempotency key">
    Collapses your retries into one transaction.
  </Step>

  <Step title="The single-use QR code">
    Even with no key at all, a scanned code cannot be charged twice — the second attempt is a
    `409`.
  </Step>

  <Step title="Replay resolution">
    A retry that arrives after the original committed is recognised as a repeat rather than
    reported as a spurious decline, even though the balance and the code have both moved on.
  </Step>
</Steps>

You should still send a key. The QR code protects against charging the *same scan* twice; only the
key protects against a retry of a request whose outcome you never learned.
