Idempotency
Network failures happen. A request may time out before you receive a response — leaving you unsure whether the server processed it. Resending the request without protection could create a duplicate ticket, charge a customer twice, or fire a notification a second time.
The BenchKey API supports idempotency keys on all mutating requests (POST, PUT, PATCH, DELETE). Send the same key on a retry and you get back the original response with no side effects repeated.
How it works
Section titled “How it works”Add an Idempotency-Key header containing a unique string to your request:
POST /api/v1/ticketsAuthorization: Bearer bk_live_acme_xxxxxxxxxxxxBenchKey-Version: 2026-06-13Content-Type: application/jsonIdempotency-Key: idem_01J8X4abc123def456- The server records the key + response when the request first completes. The fingerprint includes the HTTP method, path, and query string — a retry must be identical on all three to match.
- If the same key arrives again within 24 hours, the server replays the stored response — no second write occurs.
- After 24 hours the key expires and a new request with that key is treated as fresh.
The replay is returned with the same HTTP status code and body as the original response, plus the header:
Idempotency-Replayed: trueGenerating a key
Section titled “Generating a key”Use a cryptographically random string. A UUID v4 is the simplest choice:
curl
KEY=$(python3 -c "import uuid; print('idem_' + str(uuid.uuid4()).replace('-',''))")
curl -X POST https://app.benchkey.com/api/v1/tickets \ -H "Authorization: Bearer $BENCHKEY_API_KEY" \ -H "BenchKey-Version: 2026-06-13" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $KEY" \ -d '{ "customer_id": "cus_01J8X4abc123", "device_type": "iPhone 14", "problem": "Cracked screen", "status": "waiting" }'Node.js
import crypto from "node:crypto";
const idempotencyKey = "idem_" + crypto.randomBytes(16).toString("hex");
const res = await fetch("https://app.benchkey.com/api/v1/tickets", { method: "POST", headers: { Authorization: `Bearer ${process.env.BENCHKEY_API_KEY}`, "BenchKey-Version": "2026-06-13", "Content-Type": "application/json", "Idempotency-Key": idempotencyKey, }, body: JSON.stringify({ customer_id: "cus_01J8X4abc123", device_type: "iPhone 14", problem: "Cracked screen", status: "waiting", }),});
const ticket = await res.json();console.log(ticket);Retry pattern
Section titled “Retry pattern”Generate the key once, then reuse it across retries. Do not generate a new key per attempt — that defeats the purpose.
import crypto from "node:crypto";
async function postWithIdempotency(path, body) { const key = "idem_" + crypto.randomBytes(16).toString("hex"); const url = `https://app.benchkey.com/api/v1${path}`; const options = { method: "POST", headers: { Authorization: `Bearer ${process.env.BENCHKEY_API_KEY}`, "BenchKey-Version": "2026-06-13", "Content-Type": "application/json", "Idempotency-Key": key, // same key for every attempt }, body: JSON.stringify(body), };
let backoff = 1000; for (let attempt = 0; attempt < 5; attempt++) { try { const res = await fetch(url, options); if (res.ok) return res.json();
if (res.status === 408 || res.status === 429 || res.status >= 500) { await new Promise((r) => setTimeout(r, backoff + Math.random() * 200)); backoff = Math.min(backoff * 2, 30_000); continue; }
const { error } = await res.json(); throw new Error(`[${error.code}] ${error.message}`); } catch (err) { if (attempt === 4) throw err; await new Promise((r) => setTimeout(r, backoff)); backoff = Math.min(backoff * 2, 30_000); } }}Conflict errors
Section titled “Conflict errors”If you reuse a key with a different request body or URL, the API returns 409 Conflict with code idempotency_key_reuse:
{ "error": { "type": "conflict_error", "code": "idempotency_key_reuse", "message": "An idempotency key was reused with a different request body.", "request_id": "req_01J8X4zzzzzz" }}This is always a bug in the caller. Generate a new key for each logically distinct write.
If a concurrent request using the same key is still in flight, the API returns 409 Conflict with code idempotency_in_progress. Wait a moment and retry — the original request is still running and will complete.
Scope and limits
Section titled “Scope and limits”| Detail | Value |
|---|---|
| Applies to | POST, PUT, PATCH, DELETE |
| Key TTL | 24 hours |
| Key min length | 8 characters |
| Key max length | 255 characters |
| Header name | Idempotency-Key |
Idempotency keys are scoped to your tenant and environment (live vs. test) — two different API keys on the same tenant in the same environment share the same idempotency namespace. This means a retry with a rotated key still replays or conflicts against the original request, as intended.
When to use idempotency keys
Section titled “When to use idempotency keys”Use them on every write that has side effects you can’t easily reverse:
- Creating tickets, invoices, or estimates
- Recording a payment
- Sending an SMS or email notification
- Any
POSTthat charges money or modifies inventory
For purely idempotent operations like PUT /tickets/{id} (full replace), the header is still accepted and provides an extra safety net against network-level duplicates.
Best practices
Section titled “Best practices”- Persist the key before sending — store it alongside your pending operation so you can recover it after a crash and retry with the same key.
- Use meaningful prefixes — e.g.,
idem_ticket_oridem_pmt_to aid debugging in logs. - Never reuse across unrelated operations — one key, one logical operation.
- Combine with exponential backoff — retries on
429or5xxshould always carry the original idempotency key.