Idempotency¶
Networks drop, requests time out, answers get lost. Every Gerege money call is safe to repeat — a repeat gives one outcome, never a second payment.
1. Where Idempotency-Key goes¶
| Call | Header | Same key + same body | Same key + another body |
|---|---|---|---|
POST /payment-intents |
required (1–128 characters) | 200 — that intent (for a claimless one, the same pay_url) |
409 idempotency_conflict |
POST /payment-intents/{id}/refunds |
required | 200 — that refund |
409 idempotency_conflict |
POST /payments/{id}/confirm (the person, Gerege App) |
required | that attempt's outcome | 409 idempotency_conflict (another account) |
POST /payment-intents/{id}/capture · /cancel · /unclaim |
— | idempotent by state: already succeeded / canceled answers that |
— |
POST /payments/claim (the person) |
— | idempotent per (token, person) | — |
POST /terminals, /sub-merchants |
— | by reference / code: again → 200 that row |
— |
A key is unique per app × environment for good — it never expires. «The same body» means the same normalised body (field order and whitespace do not matter).
2. One order, one intent¶
Derive the key from your order number — not a random UUID:
Then a double-clicked order button, a backend that ran twice, overlapping retries — all one intent, one pay_url. With
a random key every retry is a new intent and the person sees two QRs. When you do need a new attempt (after an
expiry, say), use a new key: order-8812-2.
Test and live keep their keys apart.
3. Retry strategy¶
| Answer | Retry? | How |
|---|---|---|
| Network error, timeout | yes | the same key, the same body |
429 |
yes | after Retry-After, the same key |
5xx |
yes | exponential backoff (1 s, 2 s, 4 s …), the same key |
202 (processing) |
not a new call | wait for GET /payment-intents/{id} or the webhook |
409 idempotency_conflict |
no | the key was reused for another request — fix the code |
other 4xx |
no | fix the request (Error codes) |
4. Duplicate webhooks¶
Webhooks are at least once: one event may arrive twice, and order is not guaranteed. Deduplicate on the event's id
(the Gerege-Event-Id header) and make the final decision from the object's status — not from «the last event that
arrived».