Idempotency
Network errors happen. If you retry a request that creates an order or moves money, it must not run twice. Send an Idempotency-Key header and Qasimha answers a retry with the stored result of the first request instead of running it again.
How to use it
Send an Idempotency-Key header on every POST, PUT, PATCH and DELETE:
- Use a fresh UUID v4 for each new operation, such as "create order 10001" or "refund 100.00 on order X".
- Reuse the same key and the same body when you retry that operation.
- Store the key with the operation before you send it, so a retry after a crash or restart can reuse it.
curl -X POST https://api-uat.qasimha.com/api/v1/pay/session/create \
-H "X-Api-Key: $QASIMHA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 6f1c2a9e-3b7d-4e8a-9c41-2d5f8b0e7a13" \
-d @order.jsonThe header is optional. A request without it runs every time. GET requests ignore it; they are always safe to repeat.
How a key is matched
- Scope: a key belongs to your merchant account, the HTTP method and the path. The same key on a different endpoint, or from another merchant, is a different key. Retries through a different API key of the same merchant still match.
- Body: the first request's method, path and body are fingerprinted. A retry must send the byte-for-byte same body. Re-serialising the JSON with different spacing or key order counts as a different body.
- Lifetime: keys are kept for 24 hours. After that, the same key is treated as new.
What happens on a repeat
| Repeat | Response | What to do |
|---|---|---|
| Same key, same body, first request finished | The stored response, with its original HTTP status and the header Idempotent-Replay: true | Treat it as the original answer |
| Same key, first request still running | 409 REQUEST_IN_PROGRESS | Wait a moment and retry with the same key |
| Same key, different body | 422 IDEMPOTENCY_KEY_REUSED | A bug in your code. Use a new key for a new request |
| First request failed with an unexpected server error | The key is released | Retry with the same key; the request runs again |
| First request never finished (no answer after 60 seconds) | The next retry takes the key over and runs | Retry with the same key |
Things to know:
- Errors are stored too. If the first request got a
4xxfrom the endpoint, such as400 VALIDATION_ERROR,403 FORBIDDENor409 DUPLICATE_ORDER_REFERENCE, a retry with the same key and body replays that same error. Fix the request and send it with a new key; the same key with the corrected body returns422 IDEMPOTENCY_KEY_REUSED. - Rejected before the endpoint, not stored.
401(bad key) and429(rate limit) happen before the key is recorded. After fixing the key, or after waiting, retry with the same key. - The replay is the first answer. A replayed create returns the order as it was at creation, for example
status: "AwaitingCheckout". Read the order to see where it is now.
Which calls to protect
| Call | Why |
|---|---|
POST /pay/session/create, /pay/pos/create, /pay/qr/create | One basket, one order |
POST /merchant/checkout-links | One link, one SMS |
POST /merchant/orders/{id}/fulfill | A repeated partial fulfilment would count twice |
POST /merchant/orders/{id}/refund | A repeated refund would refund twice |
POST /merchant/orders/{id}/capture, /cancel, POST /merchant/checkout-links/{id}/cancel | Safe to repeat anyway, but a replay gives you the original answer |
POST, PUT, DELETE /merchant/webhooks… | One endpoint, one secret |
A retry loop
import { randomUUID } from 'node:crypto';
async function createSession(body: object) {
const key = randomUUID(); // one key for every attempt of this operation
const payload = JSON.stringify(body); // serialise once, send the same bytes each time
for (let attempt = 0; attempt < 4; attempt++) {
let res: Response;
try {
res = await fetch('https://api-uat.qasimha.com/api/v1/pay/session/create', {
method: 'POST',
headers: { 'X-Api-Key': process.env.QASIMHA_API_KEY!, 'Content-Type': 'application/json', 'Idempotency-Key': key },
body: payload,
});
} catch {
await sleep(backoff(attempt)); // network error: retry with the same key
continue;
}
if (res.status === 429 || res.status === 409 && (await peekCode(res)) === 'REQUEST_IN_PROGRESS'
|| res.status === 502 || res.status === 503 || res.status === 504) {
await sleep(retryAfterMs(res) ?? backoff(attempt));
continue;
}
return res; // success, a replay, or an error to handle
}
throw new Error('Qasimha unavailable, try again later');
}
const backoff = (n: number) => Math.min(2 ** n * 500, 8000) + Math.random() * 250;// The SDK sends a UUID v4 Idempotency-Key on every POST and reuses it across its own retries.
const session = await qasimha.checkout.createSession(request);
session.replayed; // true when the answer was a replay
// Supply your own key, for example one stored with your order attempt:
await qasimha.orders.refund(orderId, { amount: 100.00, reason: 'Return' }, { idempotencyKey: storedKey });peekCode, retryAfterMs and sleep are your own helpers: read code from a clone of the response body, read the Retry-After header, and wait.
Your own reference helps too
Give every order a unique orderReferenceId. Even with a new idempotency key, a second create with the same reference returns 409 DUPLICATE_ORDER_REFERENCE. On a payment session, when the order is yours, data includes its installmentId and paymentUrl, so you can resume the open checkout. See Choose a checkout flow.
The server SDKs use both layers: if a retried createSession gets 409 DUPLICATE_ORDER_REFERENCE naming your order, the SDK returns that order as the session, because the earlier attempt succeeded.
Troubleshooting
422 IDEMPOTENCY_KEY_REUSED on a retry
The retry's body differs from the first request's. Serialise the body once and resend the same bytes. If you changed the request on purpose, use a new key.
I fixed a validation error but keep getting the same 400
You are still sending the old key with the old body, so the stored 400 is replayed. Send the corrected request with a new key.
409 REQUEST_IN_PROGRESS keeps coming back
The first request is still running. Back off and retry. A request that has made no progress for 60 seconds is released to the next retry.
Related
Updated about 23 hours ago
