Idempotency
Safely retry POST requests with the Idempotency-Key header.
Send a unique Idempotency-Key header on any POST request and you can safely retry on failure. The server caches the first response (status code and body) and replays it on retries. Keys expire after 24 hours.
The header is required on the endpoints that create money movement or a chargeable resource - creating an account, a card, a posting, a dispute, or an API key, and submitting a card shipment. Omitting it there returns 422. Every other POST accepts it and honors it when present. The API reference marks the header as required or optional on each endpoint.
curl -X POST https://api.firstleeward.com/v0/cards \
-H "Authorization: Bearer $LEEWARD_API_KEY" \
-H "Leeward-Version: 2026-09-18" \
-H "Idempotency-Key: 7e7f1a90-3e0e-4a7e-bd2c-9b3a3c2d8e1f" \
-H "Content-Type: application/json" \
-d '{ "userId": "...", "accountId": "...", "type": "VIRTUAL" }'
Generate one key per logical operation. Use a UUIDv4 or any high-entropy string up to 255 characters. Keep the same key across every retry - never generate a new key on each attempt.
Behavior
| Situation | Response |
|---|---|
| First request | Executes and caches the response. |
| Retry, same key, same body | Returns the cached response. Includes Idempotent-Replayed: true response header. |
| Retry, same key, different body | 400 IDEMPOTENT_PARAMETER_MISMATCH - use a new key for a different request. |
| Two concurrent requests, same key | 409 IDEMPOTENCY_REQUEST_IN_PROGRESS - retry with backoff. |
| Key reused after 24 hours | Treated as a fresh request. |
All outcomes are cached, including 4xx and 5xx. If the first attempt returned a business error, retries replay that error. Fix the input and use a new key to try again.
401, 422, and 429 responses are not cached and are safe to retry with the same key.
Retry pattern
const idempotencyKey = crypto.randomUUID(); // generate once, reuse on retries
async function createCardWithRetry(body) {
for (let attempt = 0; attempt < 5; attempt++) {
try {
const res = await fetch('https://api.firstleeward.com/v0/cards', {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Leeward-Version': '2026-09-18',
'Idempotency-Key': idempotencyKey,
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
});
if (res.status === 429 || res.status >= 500) {
await sleep(2 ** attempt * 1000);
continue;
}
return await res.json();
} catch {
await sleep(2 ** attempt * 1000); // network error: safe to retry same key
}
}
}
Recommendations
- Retry on network errors and
429with the same key. These are the only cases where the original request may not have reached the server. - Use a new key after a
4xxbusiness error. The cached error replays on retries; only a new key triggers a fresh attempt. - Persist the key before sending if you need guaranteed retry safety after a client crash.
