Skip to content

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:

Idempotency-Key: order-8812          reference: order-8812

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».