Idempotency
The network occasionally drops responses, your client occasionally times out, your worker occasionally crashes mid-flight. Idempotency-Key lets you retry mutating requests without ever creating two of the same resource.
When you need it
Any non-GET request can be replayed safely if you attach an Idempotency-Key header. It's mandatory in practice for anything that creates state or moves money: creating a payment link, issuing an invoice, sending a contract for signature, marking a timesheet as approved.
Idempotency-Key as the default for every write.How it works
- The client generates a UUID v4 (or any opaque string up to 255 chars) and sends it on the request.
- On first sight, we run the request, persist the response body + status code keyed by
(api_key_id, idempotency_key), and return the response. - Any retry within 24 hours that uses the same key + same path replays the stored response. The body is byte-identical, the status code is the same, and the response carries
Idempotent-Replay: true. - After 24 hours, the entry expires. The same key with a fresh request creates a new resource — that's why you should always use a fresh UUID per logical operation.
Sending the header
curl -X POST https://api.worksible.com/v1/companies/me/payment-links \
-H "X-API-Key: $WORKSIBLE_API_KEY" \
-H "Idempotency-Key: 8c4a4e7f-9b3a-4e3f-9e60-5e1f29f0a8d9" \
-H "Content-Type: application/json" \
-d '{ "amountCents": 50000, "currency": "EUR", "description": "May retainer" }'import { randomUUID } from "node:crypto";
async function postIdempotent(path, body) {
const headers = {
"X-API-Key": process.env.WORKSIBLE_API_KEY,
"Content-Type": "application/json",
"Idempotency-Key": randomUUID(),
};
return fetch(`https://api.worksible.com${path}`, {
method: "POST",
headers,
body: JSON.stringify(body),
});
}Choosing a key
- One key per logical operation. Not per attempt. If your job creates one invoice but tries the request three times due to a flaky network, all three attempts must carry the same idempotency key.
- UUID v4 is the safe default. Random, no coordination needed, fits in 36 chars.
- Anything opaque works. If you need deterministic keys (e.g.
payout-2026-05-acmefor a daily payout job), make sure the inputs that produce the same output also produce the same key, otherwise you'll create duplicates.
Edge cases
Same key, different request body
If the same key is used twice with a different body, we return 409 idempotency_conflict and the original response stays untouched. Generate a new key for a new operation.
{
"error": "idempotency_conflict",
"reason": "Body differs from the original request stored under this Idempotency-Key.",
"originalRequestId": "5d4d7a1f-..."
}In-flight retries
If a retry arrives while the first request is still being processed (the body hasn't finished writing), we hold the second request and return the result of the first one as soon as it's ready. The retry won't observe a partial state.
After 24 hours
The store rolls. Old keys are forgotten and may be reused. Treat the window as the floor.
Detecting replays
If your client cares whether the response came from cache or from a fresh execution, look at the Idempotent-Replay response header.
HTTP/1.1 201 Created
Content-Type: application/json
Idempotent-Replay: true
X-Request-Id: a3e7-...
{ "id": "pl_abc123", ... } # the original response, byte-identicalWhat it doesn't do
- It doesn't turn an unsafe operation into a safe one. If your code inserts a row in your own database before calling the API, the database insert isn't idempotent unless you make it so on your side.
- It doesn't apply to GETs. GET requests are always safe to retry; you don't need a key.
- It doesn't bypass scope or rate-limit checks. A replayed response was already counted; we re-count it on every replay so your bucket reflects reality.