Skip to main content
All mutating API endpoints accept an Idempotency-Key header. When you retry a request with the same key, the API returns the original cached response without creating a duplicate resource. This makes retries safe by design, even when you cannot be sure whether the first attempt succeeded.

Why idempotency matters

Network failures, proxy timeouts, and server restarts can all leave you uncertain whether a request completed. Consider what happens if you create a checkout session but the response never arrives:
  • Without an idempotency key — retrying POST /checkout_sessions creates a second session. Your customer might see two different deposit addresses, and one of them will go unmonitored.
  • With an idempotency key — the second attempt returns the original session object. Only one deposit address is ever derived, and your order is linked to a single session ID.
This is especially important in webhook handlers and queue consumers, where a message may be delivered more than once.

How it works

1
Include the header on your request
2
Add Idempotency-Key: <your-unique-key> to any POST, PATCH, or DELETE request.
3
curl -X POST https://your-domain.com/api/v1/checkout_sessions \
  -H "Authorization: Bearer ck_test_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-42-v1" \
  -d '{"amount": {"value": "25.00", "currency": "USD"}}'
4
The key is stored and the response is cached
5
On the first request, the API processes it normally and caches the response body alongside your key. The cache entry is valid for 24 hours from the time of the first request.
6
Retry safely
7
If you resend the identical request within 24 hours, the API returns the cached response immediately — no new resource is created. The HTTP status code, headers, and body are identical to the original response.

Conflict detection

If you send the same Idempotency-Key with a different request body, the API returns 409 Conflict:
{
  "error": {
    "type": "conflict",
    "code": "idempotency_key_reused",
    "message": "This idempotency key was already used with a different request body.",
    "requestId": "req_01HZ..."
  }
}
This protects you from accidentally reusing a key for a completely different operation. If you genuinely want to change the request parameters, increment the version suffix in your key (see best practices below).

Best practices for choosing keys

A good idempotency key is stable (the same logical operation always produces the same key), unique (different operations produce different keys), and human-readable for debugging.
Tie the key to your internal order or intent:
// Stable: retrying for order 42 always uses the same key
const idempotencyKey = `order-${orderId}-v1`;
Append a version suffix when you intentionally change the request:
// Customer corrected the amount — this is a new intent, so use v2
const idempotencyKey = `order-${orderId}-v2`;
Use crypto.randomUUID() when you have no natural identifier:
// Generated once and persisted with your job/task record
const idempotencyKey = crypto.randomUUID();
Avoid:
  • Reusing the same static string across different orders
  • Including timestamps or random values that change on every retry (that defeats the purpose)
  • Keys longer than 255 characters

SDK usage

The TypeScript SDK accepts idempotency keys as the second argument (RequestOptions) to any mutating method:
import { CheckoutClient } from "@your-org/checkout-sdk";

const client = new CheckoutClient({
  apiKey: process.env.CHECKOUT_API_KEY!,
  baseUrl: "https://your-domain.com",
});

// Create a checkout session idempotently
const session = await client.checkoutSessions.create(
  {
    amount: { value: "25.00", currency: "USD" },
    metadata: { orderId: "42" },
    successUrl: "https://your-store.com/success",
  },
  { idempotencyKey: "order-42-v1" }, // RequestOptions — second argument
);

// Create a payment link idempotently
const link = await client.paymentLinks.create(
  { title: "Donation", fiatAmount: "10.00", fiatCurrency: "USD" },
  { idempotencyKey: "link-donation-homepage-v1" },
);

// Delete a webhook endpoint idempotently
await client.webhookEndpoints.del(
  "we_01HZ...",
  { idempotencyKey: "delete-webhook-we_01HZ-v1" },
);
The SDK sets the Idempotency-Key header automatically when idempotencyKey is provided in RequestOptions.

Which endpoints support idempotency

All POST, PATCH, and DELETE endpoints under /api/v1/* accept the Idempotency-Key header:
MethodEndpointNotes
POST/api/v1/checkout_sessionsMost critical — always use a key here
POST/api/v1/payment_links
PATCH/api/v1/payment_links/{id}
DELETE/api/v1/payment_links/{id}
POST/api/v1/webhook_endpoints
DELETE/api/v1/webhook_endpoints/{id}
POST/api/v1/webhook_endpoints/{id}/test
GET requests are inherently idempotent and do not use this header.

Quick reference

BehaviourDetails
Header nameIdempotency-Key (exact casing)
Supported methodsPOST, PATCH, DELETE
Cache TTL24 hours from first use
Same key, same bodyReturns cached response — safe
Same key, different bodyReturns 409 Conflict
Expired key, any bodyTreated as a new request