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

# Idempotency

> Send an Idempotency-Key on every money-moving call so a retry replays instead of recharging.

Send an `Idempotency-Key` header on every money-moving call. AptaPay claims
the key **before** making any provider call, so a network failure mid-request
can never leave you unable to tell whether money moved.

```http theme={null}
POST /v1/collections HTTP/1.1
Idempotency-Key: 8f14e45fceea167a5a36dedd4bea2543
Content-Type: application/json
```

Use any unpredictable unique string. A UUID with dashes stripped is fine.

## What the key guarantees

| You send                           | AptaPay does                                              |
| ---------------------------------- | --------------------------------------------------------- |
| Same key, same parameters          | Replays the original response verbatim. No second charge. |
| Same key, **different** parameters | Rejects with `422 idempotency_key_reused`.                |
| New key                            | Treats it as a genuinely new operation.                   |

The second row is the one that protects you. A key is bound to a fingerprint
of the request — the amount, the destination, the currency and the other
money-determining fields. If those differ, it is not a retry, so we refuse it
rather than guessing which one you meant.

<Warning>
  Never mint a new idempotency key to get past a `retriable` or `ambiguous`
  error. A new key restarts the whole operation as if it were unrelated —
  which is how one charge becomes two. Retry with the **same** key, always.
</Warning>

## Keys are scoped to your tenant

Your keys are yours. Another tenant using the same string cannot collide with
you, and cannot probe whether one of your keys exists.

## Choosing a key

Derive it from the operation in your own system, not from the clock:

```typescript idempotency.ts theme={null}
// Good: stable across retries, unique per real operation.
const key = `charge:${order.id}:${order.attempt}`;

// Bad: a new key on every retry defeats the entire mechanism.
const key = randomUUID(); // regenerated inside the retry loop
```

If you generate a random key, generate it **once**, store it alongside the
operation, and reuse it for every retry of that operation.

## Recovering from an unknown outcome

Because the key is claimed before the provider is contacted, "no record"
genuinely means the provider was never called. That makes recovery
deterministic:

<Steps>
  <Step title="Retry with the same key">
    If nothing moved, you get a fresh attempt. If it did move, you get the
    original response replayed rather than a second charge.
  </Step>

  <Step title="If the error is ambiguous, poll instead">
    Call `GET /v1/collections/{reference}` or `GET /v1/payouts/{reference}`
    until it reaches a terminal status. Do not reverse. See
    [Errors](/concepts/errors).
  </Step>
</Steps>

## Where it applies

Every money-moving endpoint: `POST /v1/collections`, `POST /v1/payouts`, and
`POST /v1/fx/exchange`. Quotes and reads do not need a key, though sending
one is harmless.
