Idempotency
Send an Idempotency-Key on every POST so a timed-out request can be retried safely
without minting a second QR.
The problem it solves
Your request to create a payment times out. You have no idea whether Qora minted the QR before the connection dropped. Retry blindly and you may create two payments for one order; do nothing and the customer has nothing to scan.
An idempotency key removes the guesswork: retry the identical request and you get the identical response back, whether or not the first attempt landed.
Usage
curl https://qora.laayo.com.np/v1/payments \
-H "Authorization: Bearer sk_test_YOUR_KEY" \
-H "Idempotency-Key: order-1042-attempt-1" \
-d amount=25000
Any unique string up to 120 characters. Derive it from something stable in your own system — an order ID is ideal, because the natural retry uses the same value automatically.
// Stable across retries of the same logical operation, unique across orders.
$key = 'order-' . $order->id . '-payment-attempt-' . $order->payment_attempts;
Do not use a random UUID generated at call time. A retry would produce a new key, which is exactly the situation idempotency exists to prevent.
What happens on replay
| Situation | Response |
|---|---|
| Key never seen | Request runs normally; the response is stored against the key. |
| Key seen, request finished | The original response, byte for byte, with Idempotent-Replay: true. |
| Key seen, request still running | 409 idempotency_in_progress. Wait a moment and retry. |
| Key reused with a different body | 409 idempotency_key_reuse. |
The last row is deliberate. Reusing a key with a different amount is a bug in the caller, and answering it with the previous payment would hide the bug behind a plausible-looking success.
Retention
Keys are kept for 24 hours, which covers any sane retry schedule. After that the same key is treated as new.
Only responses below 500 are stored. A server error is not cached, so a transient
failure on our side never pins itself to your key and block the operation for a day.
Retrying correctly
$key = 'order-' . $order->id;
for($attempt = class="tok-num">1; $attempt <= class="tok-num">3; $attempt++) {
class="tok-key">try {
// Same key every time: at most one payment is ever created.
class="tok-key">return $api->post('/v1/payments', [
'amount' => $order->total_paisa,
], ['Idempotency-Key' => $key]);
} class="tok-key">catch (TimeoutException | ServerException $e) {
class="tok-key">if ($attempt === class="tok-num">3) {
class="tok-key">throw $e;
}
sleep(class="tok-num">2 ** $attempt);
}
}
Reads
GET requests are naturally idempotent and ignore the header. Only
POST /v1/payments and POST /v1/payments/{id}/cancel use it.