Core concepts

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.

Without it, retries are dangerous
If your client receives a 502 from a transient gateway error, you genuinely don't know whether the resource was created. Retrying without an idempotency key may double-create. Treat Idempotency-Key as the default for every write.

How it works

  1. The client generates a UUID v4 (or any opaque string up to 255 chars) and sends it on the request.
  2. On first sight, we run the request, persist the response body + status code keyed by(api_key_id, idempotency_key), and return the response.
  3. 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.
  4. 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
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" }'
node.js · helper
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-acme for 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.

json
{
  "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
HTTP/1.1 201 Created
Content-Type: application/json
Idempotent-Replay: true
X-Request-Id: a3e7-...

{ "id": "pl_abc123", ... }   # the original response, byte-identical

What 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.