Skip to content
Ask the docs

Find answers across the QairoPay docs.

Type a question and we'll synthesize an answer from the docs with citations back to the source pages.

Idempotency

Network calls fail. Retrying a failed POST without idempotency is the fastest way to mint two passes when you wanted one. Idempotency makes retries safe.

How it works

For every state-changing request (POST, PATCH, DELETE against an unstable id), send a unique Idempotency-Key header:

Terminal window
curl https://api.qairopay.com/v1/passes \
-H "Authorization: Bearer $QAIROPAY_KEY" \
-H "Idempotency-Key: 9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d" \
-H "Content-Type: application/json" \
-d '{ ... }'

QairoPay stores the result (status code, headers, body) of the first request keyed by (tenant_id, idempotency_key). Any subsequent request with the same key, within the retention window, returns the stored result without re-running the operation.

Conventions

  • Use UUID v4 or a ULID. uuidgen and crypto.randomUUID() both work.
  • One key per logical operation. If you’re issuing one pass, generate one key, and reuse it on every retry of that pass.
  • A new logical operation needs a new key. Don’t reuse keys across distinct operations.
  • Keys are tenant-scoped. The same key string is independent across tenants.
  • Keys are case-sensitive and limited to 255 characters. [a-zA-Z0-9_-] only.

With the TypeScript SDK

@qairopay/sdk handles idempotency for you. Every write call (POST, PATCH, DELETE) gets a UUID v4 attached automatically, and the same key is reused on every retry of the same logical operation — so 5xx retries can never duplicate writes:

import { QairoPay } from "@qairopay/sdk";
const qp = new QairoPay({ apiKey: process.env.QAIROPAY_KEY! });
// No idempotencyKey provided → SDK generates one and reuses it
// on every retry the transport performs internally.
const pass = await qp.passes.create({
template_id: "tpl_01HZX...",
holder: { email: "[email protected]" },
});
// Need to control the key (e.g., derive it from your own request id)?
// Pass it explicitly:
const passWithKey = await qp.passes.create(
{ template_id: "tpl_01HZX...", holder: { email: "[email protected]" } },
{ idempotencyKey: "my-order-id-12345" },
);

The SDK never attaches Idempotency-Key on reads (GET/HEAD/OPTIONS) — reads are inherently idempotent.

Retention

Idempotency results are retained for 24 hours after the original request completes. Retries after that window are treated as new requests. If you need a longer retention, request an extension via your account contact.

What counts as a “match”

A retry must be byte-identical to the original request body. If the body differs, QairoPay returns:

HTTP/1.1 409 Conflict
{
"error": {
"type": "idempotency_conflict",
"message": "Idempotency-Key was reused with a different request body.",
"param": "Idempotency-Key"
}
}

This is intentional: if you sent a different body, you almost certainly meant to perform a different operation and should generate a new key.

In-flight requests

If a retry arrives while the original is still in flight, the second request returns:

HTTP/1.1 409 Conflict
{
"error": {
"type": "idempotency_inflight",
"message": "A request with this key is already in progress. Wait and retry.",
"retry_after_ms": 250
}
}

Honor retry_after_ms with exponential backoff. The SDKs do this automatically.

When you don’t need a key

GET, HEAD, and OPTIONS requests are idempotent by HTTP semantics — no key needed. PUT operations against a known id (PUT /v1/passes/pass_XXX) are also naturally idempotent and ignore the header if present.

Patterns

A clean pattern: generate the key once when the user takes the action, persist it alongside your domain object, and reuse it on every retry until the operation succeeds.

async function issueLoyaltyPass(userId: string) {
const idempotencyKey =
(await db.passIssuance.findKey(userId)) ??
(await db.passIssuance.createKey(userId, crypto.randomUUID()));
return qp.passes.create({ /* ... */ }, { idempotencyKey });
}

This makes your client crash-safe: even if your process dies between sending the request and recording the response, restarting will retry with the same key and pick up the original result.