How to Use Idempotency Keys
Idempotency keys prevent duplicate operations when a request is retried due to network failures, timeouts, or client-side errors. This is critical for endpoints that create resources or trigger financial transactions.
Why Idempotency Matters
Without idempotency protection, retrying a failed request can result in:
- Duplicate charges -- a customer is billed twice for the same purchase
- Duplicate resources -- two identical discounts are redeemed
- Inconsistent state -- parallel retries create conflicting records
Idempotency keys solve this by letting the server recognize repeated requests and return the original response instead of executing the operation again.
Environment Variables
Set these variables before running any example on this page:
export BASE_URL="https://api.chainabit.com/api/v1"
export TOKEN="your-access-token"Which Endpoints Require It
The following endpoints require (or strongly recommend) an Idempotency-Key header:
| Endpoint | Why |
|---|---|
POST /billing/checkout | Prevents duplicate charges |
POST /billing/discounts:redeem | Prevents redeeming a discount code twice |
Other POST endpoints accept the header but do not require it. It is good practice to include it on any mutating request where a duplicate would cause problems.
How to Use It
Send the Idempotency-Key header with a unique identifier. A UUID v4 is recommended.
Example: Checkout
curl -s -X POST "https://api.chainabit.com/api/v1/billing/checkout" \
-H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"productCode": "pro-monthly"
}'const BASE_URL = process.env.BASE_URL;
const TOKEN = process.env.TOKEN;
const response = await fetch(`${BASE_URL}/billing/checkout`, {
method: "POST",
headers: {
Authorization: `Bearer ${TOKEN}`,
"Idempotency-Key": crypto.randomUUID(),
"Content-Type": "application/json",
},
body: JSON.stringify({
productCode: "pro-monthly",
}),
});
const { data } = await response.json();import requests
import uuid
import os
BASE_URL = os.environ["BASE_URL"]
TOKEN = os.environ["TOKEN"]
response = requests.post(
f"{BASE_URL}/billing/checkout",
headers={
"Authorization": f"Bearer {TOKEN}",
"Idempotency-Key": str(uuid.uuid4()),
},
json={"productCode": "pro-monthly"},
)
data = response.json()["data"]Response (first request):
{
"data": {
"checkoutId": "chk_01HQX...",
"checkoutUrl": "https://checkout.chainabit.com/session/chk_01HQX...",
"expiresAt": "2026-03-17T11:00:00.000Z"
}
}If you send the exact same request with the same idempotency key, the server returns the original response with a 200 status code instead of creating a new checkout session.
Example: Redeem a Discount
curl -s -X POST "https://api.chainabit.com/api/v1/billing/discounts:redeem" \
-H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"code": "LAUNCH2026"
}'const BASE_URL = process.env.BASE_URL;
const TOKEN = process.env.TOKEN;
const response = await fetch(`${BASE_URL}/billing/discounts:redeem`, {
method: "POST",
headers: {
Authorization: `Bearer ${TOKEN}`,
"Idempotency-Key": crypto.randomUUID(),
"Content-Type": "application/json",
},
body: JSON.stringify({
code: "LAUNCH2026",
}),
});
const { data } = await response.json();import requests
import uuid
import os
BASE_URL = os.environ["BASE_URL"]
TOKEN = os.environ["TOKEN"]
response = requests.post(
f"{BASE_URL}/billing/discounts:redeem",
headers={
"Authorization": f"Bearer {TOKEN}",
"Idempotency-Key": str(uuid.uuid4()),
},
json={"code": "LAUNCH2026"},
)
data = response.json()["data"]Generating Idempotency Keys
Use any UUID v4 generator. Here are a few options:
# Linux / macOS
uuidgen// Node.js 19+ / modern browsers
crypto.randomUUID()
// Older Node.js
require('crypto').randomUUID()import uuid
print(uuid.uuid4())Rules
One key per logical operation. Generate a new key for each distinct user action. Do not reuse keys across different operations.
Same key = same request body. If you retry with the same idempotency key but a different request body, the server returns a
422 Unprocessable Entityerror.Keys expire after 24 hours. After expiry, the same key can be used for a new operation. Do not rely on this -- always generate fresh keys.
Keys are scoped to your account. Two different accounts can use the same key value without conflict.
Error Responses
| Status | Meaning |
|---|---|
200 | Idempotent replay -- returning the original response |
409 Conflict | A request with this key is currently being processed |
422 Unprocessable Entity | The key was already used with a different request body |
Conflict Example
If a previous request with the same key is still in progress:
{
"statusCode": 409,
"message": "A request with this idempotency key is already being processed",
"error": "Conflict"
}Wait briefly and retry. The original request will complete and subsequent retries will return the cached response.