Idempotency and retries
A network retry can resend a request whose first attempt already succeeded. To make that safe, every authenticated POST takes an Idempotency-Key header. The same key with the same request returns the original response, so the work happens once.
Every request on this page runs in CI against an in-process core on FakeChain, so a response that stops matching its # expect line fails the build. The {{KEY}} placeholder works as in the quickstart.
Send the key
Idempotency-Key is required on every POST; a POST without it is rejected with idempotency_key_missing before any work starts. A key is scoped to the API key that sends it, kept for 24 hours, and bound to a hash of the method, path and body. The key is a header, not part of the body, so it is set on POST /v1/counterparties just as on any other write.
POST /v1/counterparties
authorization: Bearer {{KEY}}
idempotency-key: idempotency-guide-bank
content-type: application/json
{
"type": "bank_account",
"label": "Idempotency guide",
"beneficiary_name": "Ada Lovelace",
"bank_name": "Analytical Bank",
"account_number": "123456789",
"routing_code": "021000021",
"currency": "USD"
}
# expect 201
# save CPTY = $.id
Replay the request
Send the byte-identical request again with the same key. The server returns the stored response — the same status and the same body — and adds Idempotent-Replayed: true to the headers. It does not create a second counterparty.
POST /v1/counterparties
authorization: Bearer {{KEY}}
idempotency-key: idempotency-guide-bank
content-type: application/json
{
"type": "bank_account",
"label": "Idempotency guide",
"beneficiary_name": "Ada Lovelace",
"bank_name": "Analytical Bank",
"account_number": "123456789",
"routing_code": "021000021",
"currency": "USD"
}
# expect 201
# save REPLAYED = $.id
Both responses name the same counterparty, so a retry is safe.
The four outcomes
| Situation | Response |
|---|---|
| First request with a key | The request runs, and the response is stored. |
| Same key, same method, path and body | The stored response, with Idempotent-Replayed: true. |
| Same key, a different request | 422 idempotency_key_reused. |
| A concurrent request holds the key | 409 idempotency_request_in_progress. |
Wait for the first request to finish before retrying with the same key; a key that is briefly in flight is the concurrent case, not a replay.
Retry with the SDK
retryingFetch from @scr/sdk wraps fetch and retries 429, 500, 502, 503 and 504. On a POST it sets a fresh Idempotency-Key and reuses it on every attempt, so a retried transfer is still one transfer. It honours Retry-After where the server sent one, caps the wait at 30 seconds, and otherwise backs off exponentially from 200 ms. createScrClient uses it by default.
import { retryingFetch } from "@scr/sdk";
const doFetch = retryingFetch({ apiKey: process.env.SCR_API_KEY! });
const res = await doFetch(
new Request("https://issuer.example/v1/transfers", { method: "POST", body: JSON.stringify(body) }),
);
A key lasts 24 hours. After that the stored response expires and the same key counts as new, so a retry that spans a longer outage is executed again rather than replayed. Do not retry a money-moving request with an old key after 24 hours: check the state of the original transfer first (for example by listing transfers). Let the SDK choose the key for ordinary retries; if you choose the key yourself, reuse it for every attempt inside the 24-hour window.