Idempotency and retries
Retry a create without creating twice. One key per operation, the same key on every retry.
retries-idempotency.sh
# A key derived from the order: a retry (a timeout, a 5xx, a job re-run after a crash) sends the# same one and gets the first answer again, with Idempotent-Replayed: true, instead of a second# checkout. Retry 429s after Retry-After, with the same key.curl https://api.qbitflow.app/v2/checkout-sessions/payment \ -H "X-API-Key: $QBITFLOW_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: checkout-order-1042" \ -d '{ "productName": "T-shirt", "description": "Blue, size M", "priceUsd": "4.99", "reference": "order-1042" }'# 201: a CreatedCheckoutSession ({ "link": "https://…/checkout?uuid=pay@…", "uuid": "pay@…", "expiresAt": "…" })errors-and-retries.ts
// Reads and the 6 creates are retried on network errors, 5xx and 429, waiting 1 s, 2 s, 4 s…// (default: 3 retries; 0 turns them off). timeout bounds each attempt, in milliseconds.const client = QBitFlow.fromEnv({ maxRetries: 5, timeout: 15_000 }); // Each create sends an Idempotency-Key, the same on its retries. A key derived from the order// also covers a job re-run after a crash: the same key returns the first session instead of// opening a second one.const orderId = 'order-1042';const session = await client. checkoutSessions. createPayment( { productName: 'T-shirt', description: 'Blue, size M', priceUsd: '4.99', reference: orderId }, { idempotencyKey: `checkout-${orderId}` });console.log(`Redirect the customer to ${session.link}`);errors_and_retries.py
# Network errors, timeouts, 5xx and 429 are retried with back-off (3 retries by default).client = qbitflow. QBitFlow. from_env( max_retries=5) order_reference = "order-1042"session = client. checkout_sessions. create_payment( product_name="T-shirt", description="Blue, size M", price_usd="4.99", # a decimal string, never a float reference=order_reference, # A key derived from the order: any retry, even from another process after a crash, # returns this same checkout instead of creating a second one. options=qbitflow. RequestOptions( idempotency_key=f"checkout-{order_reference}"),)print("Send the customer to", session.link)errors_and_retries/main.go
// Reads and creates are retried on network errors, 5xx and 429 (1 s, 2 s, 4 s…);// WithMaxRetries(0) turns that off. The client reads QBITFLOW_API_KEY.client, err := qbitflow. NewClientFromEnv( qbitflow. WithMaxRetries( 5))if err != nil { log.Fatal(err)} // Each create sends an Idempotency-Key, the same on its retries. Derive your own from the// order to retry across processes: the same key returns the same session, never a second one.orderID := "order-1042"session, err := client. CheckoutSessions. CreatePayment( ctx, &qbitflow. CreatePaymentSessionParams{ ProductName: "T-shirt", Description: "Blue, size M", PriceUSD: "4.99", Reference: orderID, SuccessURL: "https://shop.example.com/orders/success?uuid=" + qbitflow.PlaceholderUUID, CancelURL: "https://shop.example.com/orders/cancel",}, qbitflow. WithIdempotencyKey( "checkout-"+orderID))var reused *qbitflow.IdempotencyErrorif errors.As(err, &reused) { log.Fatal("this key was already used with other params") // 422 idempotency_key_reused}if err != nil { log.Fatal(err)}fmt.Println("Redirect the customer to", session.Link)errors-and-retries.php
// Reads and creates are retried on network errors, 5xx and 429 (3 retries by default).$client = \QBitFlow\QBitFlow::fromEnv( maxRetries: 5); $orderId = 'order-1042';$session = $client->checkoutSessions->createPayment( new \QBitFlow\Params\CreatePaymentSessionParams( productName: 'T-shirt', description: 'Blue, size M', priceUsd: '4.99', reference: $orderId, successUrl: 'https://shop.example.com/orders/success?uuid=' . \QBitFlow\Placeholders::UUID, cancelUrl: 'https://shop.example.com/orders/cancel', ), // The same key returns the same session, even from another process after a crash. new \QBitFlow\RequestOptions( idempotencyKey: 'checkout-' . $orderId),);echo "Pay at {$session->link}\n";The SDKs retry for you, one key per operation. Derive it from your own order
(checkout-order-1042) and a crash or a re-run is covered too.
- Your server → QBitFlow API: Creates, with Idempotency-Key: K
- QBitFlow API → Your server: 201: created
- Your server → QBitFlow API: The same request again, with K
- QBitFlow API → Your server: The first answer, Idempotent-Replayed: true
- Your server → QBitFlow API: Another body, with K
- QBitFlow API → Your server: 422 idempotency_key_reused
When to retry
After a timeout, a dropped connection, a 5xx or a 429: with the same key. A 4xx comes back the
same until you fix the request (nothing is kept after an error, so the same key runs the fixed one).
The rules
- One key per operation: 1 to 255 printable ASCII characters, no spaces. A UUID v4 or a value built from your order.
- Same key, same request, within 24 hours: the first answer again (same
status, body and uuid), with
Idempotent-Replayed: true. Nothing is created twice. - Same key, another request:
422 idempotency_key_reused, nothing done. - Same key, first request still running:
409 idempotency_key_in_use. Retry once it answered. - Only successes are kept. After a
400, a409or a5xx, the key runs the request again. - Keys are per API key, in its space. No other key, nor the dashboard, gets your answer.
- The body compared is at most 1 MiB:
413 request_too_largeabove.
The routes that take a key
Among the routes an API key calls:
POST /checkout-sessions/
Codes to expect
idempotency_HTTP 409key_ in_ use the
Idempotency-Key's first request is still running (idempotency): retry once it answeredWhat to do: Retry once the first request has answered, with the same key.
idempotency_HTTP 422key_ reused the
Idempotency-Keywas used for another request (idempotency): send a new keyWhat to do: Send a new key: one key names one operation.
request_HTTP 413too_ large an
Idempotentroute's body over 1 MiB (idempotency)