Skip to content

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.

Add an Idempotency-Key header containing a unique string to your request:

POST /api/v1/tickets
Authorization: Bearer bk_live_acme_xxxxxxxxxxxx
BenchKey-Version: 2026-06-13
Content-Type: application/json
Idempotency-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: true

Use a cryptographically random string. A UUID v4 is the simplest choice:

curl

Terminal window
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);

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);
}
}
}

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.

DetailValue
Applies toPOST, PUT, PATCH, DELETE
Key TTL24 hours
Key min length8 characters
Key max length255 characters
Header nameIdempotency-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.

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 POST that 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.

  • 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_ or idem_pmt_ to aid debugging in logs.
  • Never reuse across unrelated operations — one key, one logical operation.
  • Combine with exponential backoff — retries on 429 or 5xx should always carry the original idempotency key.