Skip to content

Idempotency

Send Idempotency-Key: <uuid> on any POST and the gateway will replay the cached response when the same request arrives again within 24 hours. This makes network-blip retries safe and prevents double charges.

  • The gateway hashes the request body and stores (idempotency_key, body_hash) → cached_response in Redis with a 24-hour TTL.
  • A second request with the same key and same body returns the cached response, with the same HTTP status, same transaction_id, same everything. No new transaction is created.
  • A second request with the same key and a different body returns 409 Conflict.
  • A second request with a different key is treated as new and may create a new transaction.
1st POST key=A body={amount:12.50} → 200, txn_xyz, charges card
2nd POST key=A body={amount:12.50} → 200, txn_xyz, idempotent replay
3rd POST key=A body={amount:13.00} → 409, idempotency key reused with different body
4th POST key=B body={amount:12.50} → 200, txn_NEW, second charge

Use a UUID v4 per logical request, not per HTTP attempt. The same key must be re-sent if you retry. Examples of good keys:

  • crypto.randomUUID() saved alongside your order in your database before you make the first call. Re-sent on every retry of that order’s checkout.
  • A deterministic hash of (order_id, attempt_number) — but only if attempt_number doesn’t change between retries.

Never use:

  • Date.now() — it changes per request, which defeats the purpose.
  • The order ID alone — if a customer retries an abandoned checkout an hour later, you don’t want the cached failure response.
EndpointIdempotency-Key honoured?
POST /internal/sessions/createYes
POST /payments/collectYes
POST /payments/collect-redirectYes
POST /payments/sendYes
POST /payments/submitYes
POST /payments/verify-otpHonoured but rarely needed (OTP codes are single-use)
GET ...N/A — GET is naturally idempotent

The Flutter SDK auto-attaches Idempotency-Key (UUID v4) to every POST it issues. You can pre-set one via Options.headers if you need to control it externally.

The Angular SDK does not auto-attach Idempotency-Key — set it manually via PdirectHttpClientService if you need it:

// Pseudocode — until Angular SDK v2 wires this automatically
this.http.configure({
customHeaders: { "Idempotency-Key": crypto.randomUUID() },
});

A cache hit doesn’t count against your rate-limit budget. The order is:

Request → RateLimiter → IdempotencyMiddleware → Handler
├─ cache hit? → return cached response
└─ cache miss? → invoke handler, cache result

So replaying through a flaky network does not eat into your 30/min budget.

  • Different body, same key → 409. The error envelope’s error_code is "1110" (INVALID_FIELD_FORMAT) and the message will mention idempotency reuse. Pick a new UUID.
  • Cached error responses. If your first attempt failed with a 4xx, the cache stores the failure. Retrying with the same key replays the failure. To genuinely retry the operation, use a new key.
  • TTL is 24 h. After that the key is forgotten. Long-tail duplicate prevention requires your own database deduplication.
  • Errors1110 / 1301 / retry semantics
  • Rate limits — why idempotent replays don’t cost