Skip to content

Safe retries

Every write (POST, PATCH) needs two headers on top of authentication:

HeaderRule
Idempotency-Key≤ 128 characters, unique per logical request. A ULID or UUID. Missing → 400.
X-Request-IdA ULID (preferred) or UUID, unique per attempt. Missing on a write → 400.

The rule

On a timeout, a 5xx or a dropped connection: resend the same Idempotency-Key with the same body. A new key on retry is how a payment gets made twice.

Each attempt still gets a fresh X-Timestamp, X-Signature and X-Request-Id.

SituationYou get
First submitThe normal response, Idempotent-Replayed: false
Same key, same body, first one finishedThe stored response, byte for byte, Idempotent-Replayed: true
Same key, first one still running409 — wait, then retry the same key
Same key, different body422 — the key is spent
Previous attempt failedThe key is free; the resubmit runs

"Same body" is compared after sorting JSON object keys, so a library that re-orders fields on retry is fine. Keys live 24 hours.

ts
const idempotencyKey = ulid(); // 1. persist with your own record
for (let attempt = 1; attempt <= 3; attempt++) {
  const request = buildRequest(credentials, {
    method: "POST",
    pathWithQuery,
    json,
    idempotencyKey,
  });
  const response = await fetch(base + pathWithQuery, request).catch(() => undefined);
  if (response && response.status < 500 && response.status !== 409) return response;
  await sleep(attempt * 1000); // 2. same key, fresh signature
}

A computation that writes nothing — POST /fees/quote — takes no Idempotency-Key.

Request ids

Every response echoes X-Request-Id — errors included. Quote it when you contact support; it is on every log line and audit event for that request. On reads it is optional and generated for you.