How-to

Idempotency and retries

Retry a create without creating twice. One key per operation, the same key on every retry.

Retries and idempotency keys

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": "…" })
// 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}`);
# 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)
// 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)
// 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.

  1. Your server → QBitFlow API: Creates, with Idempotency-Key: K
  2. QBitFlow API → Your server: 201: created
  3. Your server → QBitFlow API: The same request again, with K
  4. QBitFlow API → Your server: The first answer, Idempotent-Replayed: true
  5. Your server → QBitFlow API: Another body, with K
  6. 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, a 409 or a 5xx, 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_large above.

The routes that take a key

Among the routes an API key calls: POST /checkout-sessions/payment, POST /checkout-sessions/subscription, POST /products, POST /customers, POST /webhooks/endpoints and POST /invitations. Other routes ignore the header; reads are safe to retry as they are.

Codes to expect

  • idempotency_key_in_useHTTP 409

    the Idempotency-Key's first request is still running (idempotency): retry once it answered

    What to do: Retry once the first request has answered, with the same key.

  • idempotency_key_reusedHTTP 422

    the Idempotency-Key was used for another request (idempotency): send a new key

    What to do: Send a new key: one key names one operation.

  • request_too_largeHTTP 413

    an Idempotent route's body over 1 MiB (idempotency)