34 — Developer
Idempotency
Networks fail mid-request. Idempotency keys make that boring: send the same POST twice and you get the same payment once — never a double charge.
How it works
Attach an Idempotency-Key header to any POST. The key is a string you generate, up to 255 characters — a UUID, or something meaningful like the order id:
curl https://api.finscale.dev/v1/payments \
-H "Authorization: Bearer sk_test_51FinscaleDemo…" \
-H "Idempotency-Key: idem_ord_9f21_0716" \
-H "Content-Type: application/json" \
-d '{
"amount": 4900,
"currency": "EUR",
"payment_method": "ideal",
"reference": "ord_9f21_0716",
"customer": { "email": "anna@example.com" },
"return_url": "https://shop.example.com/checkout/return"
}'
The first request with a given key executes normally, and Finscale stores its full response. Any later request with the same key and the same payload gets that stored response back — the payment is not created twice. Replays are flagged with a response header:
HTTP/2 201
Idempotency-Key: idem_ord_9f21_0716
{ "id": "pay_8Q2mX4nT1cVb", "status": "requires_action", … }
Keys are scoped per account and per mode — a test-mode key never collides with a live-mode key. GET and DELETE requests are naturally idempotent and ignore the header.
The 24-hour window
Stored responses are kept for 24 hours from first use. Within that window, the same key + same payload always replays the original response — including error responses (a 402 decline replays as the same 402). After the window expires, the key is forgotten and behaves like a fresh one.
Two consequences worth designing for:
- Retry loops (queues, workers, cron) are safe as long as they re-run within 24 hours — which is what you want for delivery guarantees.
- Don't reuse business-meaningful keys across days.
idem_ord_9f21_0716is safe because an order is charged once; a key likeidem_daily_billingwould silently start double-charging after 24 hours.
Conflicts: same key, different payload
Reusing a key with a different body is always a bug, so Finscale refuses rather than guesses:
{
"error": {
"type": "idempotency_error",
"code": "idempotency_key_reused",
"message": "Idempotency-Key idem_ord_9f21_0716 was used with a different request payload.",
"request_id": "req_7Hf3kQd2"
}
}
If a request is still in flight when a duplicate with the same key arrives, the duplicate waits for the first to finish and then replays its response — you never race yourself into two payments.
Retry recipes
Generate the key with the operation, not the request
Create the key when the logical operation is born (order checkout, refund request) and persist it alongside. Every retry of that operation — same process or a different one after a crash — sends the same key.
// once, when the order is created
const order = {
id: "ord_9f21_0716",
amountMinor: 4900,
currency: "EUR",
idempotencyKey: "idem_ord_9f21_0716" // persisted with the order row
};
// every attempt, first or retried, uses the stored key
const res = await fetch("https://api.finscale.dev/v1/payments", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.FINSCALE_SECRET_KEY}`,
"Idempotency-Key": order.idempotencyKey,
"Content-Type": "application/json"
},
body: JSON.stringify({
amount: order.amountMinor,
currency: order.currency,
payment_method: "ideal",
reference: order.id,
return_url: "https://shop.example.com/checkout/return"
})
});
Retry on timeouts and 5xx, with backoff
A timeout doesn't tell you whether the payment was created — the replay guarantee means you don't have to care. Retry with the same key and you either complete the original or get its stored response:
- Retry on: network timeouts, connection resets,
api_error(5xx),rate_limit_error(429, afterRetry-After). - Don't retry on: 400, 401, 402, 404 — the answer won't change. See error types.
- Use exponential backoff with jitter: e.g. 1s, 2s, 4s, 8s (±20%), cap at 5 attempts, then alert.
One logical operation, one key — forever
New attempt at a different charge (the customer retries checkout with another payment method after a hard decline)? That's a new operation: generate a new key. Replaying idem_ord_9f21_0716 with a changed payment_method would return the stored decline — or a 409 — not a fresh authorization. A simple convention that scales: idem_<order>_<attempt>.
Every POST that moves money carries an idempotency key, every key is persisted with the thing it pays for, and every retry reuses it verbatim. Do that and duplicate charges become structurally impossible.