# Idempotency

Send an `Idempotency-Key` header on every request that creates something. It is one line of code, and it is the difference between a retry being safe and a retry being a double charge.

## The problem it solves

<Mermaid chart={`
sequenceDiagram
    autonumber
    participant You as Your server
    participant KN as Kwik Nkap
    You->>KN: POST /v1/payments (Idempotency-Key: abc-123)
    KN->>KN: Store key, create payment
    KN--xYou: 201 lost — connection dropped
    Note over You: You never saw an answer.<br/>Did it arrive?
    You->>KN: Retry, same Idempotency-Key: abc-123
    KN->>KN: Key already seen
    KN-->>You: 201 — the ORIGINAL response<br/>Idempotent-Replayed: true
    Note over You,KN: One payment. One debit.
`} />

The dangerous moment is not failure, it is *uncertainty*. A timeout tells you nothing about whether the request arrived. Without a key you have two bad options: retry and risk charging twice, or do not retry and risk losing a payment you may have already taken.

The key removes the dilemma. Retry freely; at most one payment exists.

## Using it

```bash
curl -X POST https://api.kwiknkap.com/v1/payments \
  -H "X-API-Key: kn_sk_test_..." \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{ "amount": 5000, "phone_number": "237670000001", "payment_method": "mobile_money" }'
```

Generate the key **once per operation**, before the first attempt, and reuse the same value for every retry of that operation. A UUID is ideal.

Generating it inside your retry loop defeats the whole mechanism: each attempt looks like a new operation, and you are back to double charges.

## What you get back

A replay returns the **original** response — same status, same body, same payment id — plus a header:

```http
HTTP/1.1 201 Created
Idempotent-Replayed: true
```

Use it in logs when you are working out whether a spike was real traffic or a retry storm. Your business logic should not need it: the point is that a replay is indistinguishable from the first call.

## The rules

**Keys live 24 hours.** Long enough for any sane retry policy. After that the key is forgotten and a request carrying it is treated as new.

**Keys are scoped to your business.** Yours cannot collide with another merchant's.

**Same key, different body is an error.** It means a bug on your side — two different operations sharing a key — and we would rather tell you than guess which one you meant. You get a `409`.

**Concurrent requests with the same key also `409`.** The first is still in flight. Retry after it settles; you will get the replay.

## Where it applies

Any call that creates something and would be expensive to duplicate:

| Endpoint | Why |
| --- | --- |
| `POST /v1/payments` | A duplicate debits the customer twice |
| `POST /v1/invoices` | A duplicate sends two bills for one order |
| `POST /v1/payment-links` | A duplicate leaves an orphan link live |

Reads (`GET`) are naturally idempotent and ignore the header. `POST /v1/invoices/{id}/send` is deliberately safe to repeat — resending is how you chase a late payment.

## Things that surprise people

**It is not deduplication of your business logic.** Two genuinely different orders that happen to have the same amount and phone number are two payments. Only an identical key collapses them.

**A `409` is a bug signal, not a transient error.** Do not retry through it with a fresh key until you understand which two operations shared one.

**24 hours is not a reconciliation window.** For anything older, reconcile on your own `external_reference` and our payment id.

## Next steps

- [How a payment works](/en/concepts/how-payments-work) — why a create call can time out mid-flight
- [Payment states](/en/concepts/payment-states) — making fulfilment exactly-once on your side too
- [Create a payment](/en/api/direct-payments) — the header on the reference
