Qora by Laayo
Get API keys

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

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

A good key
// 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

SituationResponse
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

Retry with backoff
$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.