Appearance
Safe retries
Every write (POST, PATCH) needs two headers on top of authentication:
| Header | Rule |
|---|---|
Idempotency-Key | ≤ 128 characters, unique per logical request. A ULID or UUID. Missing → 400. |
X-Request-Id | A 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.
| Situation | You get |
|---|---|
| First submit | The normal response, Idempotent-Replayed: false |
| Same key, same body, first one finished | The stored response, byte for byte, Idempotent-Replayed: true |
| Same key, first one still running | 409 — wait, then retry the same key |
| Same key, different body | 422 — the key is spent |
| Previous attempt failed | The 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.