Idempotency & retries
Safely retry mutating Fynex API calls without creating duplicates.
Networks fail. Servers restart. The right response to a flaky call is to retry safely — and that's what idempotency keys are for.
How it works
Every mutating endpoint accepts the Idempotency-Key header (a UUID you generate). Fynex remembers the response body for that key and replays it on subsequent calls with the same key, so you can retry without creating duplicate payments, duplicate refunds, or duplicate payouts.
Where keys are accepted
| Endpoint | Header | Purpose |
|---|---|---|
POST /checkout |
Idempotency-Key |
Don't create duplicate hosted sessions |
POST /initialize-payment |
Idempotency-Key |
Don't double-charge |
POST /finalize-payment |
Idempotency-Key |
Idempotent capture |
POST /payments/{id}/capture |
Idempotency-Key |
Don't double-capture |
POST /payments/{id}/refund |
Idempotency-Key |
Don't double-refund |
POST /payouts |
Body field idempotencyKey |
Don't double-pay out |
POST /payees |
Idempotency-Key (optional) |
Don't create a second payee |
POST /payees/setup |
Idempotency-Key (optional) |
Don't create a second payee and bank account |
POST /payees/{id}/payout-methods |
Idempotency-Key (optional) |
Don't add a second bank account |
POST /webhooks |
Idempotency-Key (optional) |
Get the signing secret back after a lost response |
POST /split-rules |
Idempotency-Key (optional) |
Don't create a second rule |
Replay semantics depend on the endpoint — see each endpoint's docs. On the five create routes above that mark it optional — payees, payee setup, payout methods, webhooks, split rules — the key may be any string of at most 255 characters, and its namespace is your account. A retry with the same key and the same body answers 201 with what the first request created, as it is now (a webhook includes its current signing secret). The same key with a different body, or on another route, answers 409 with code idempotency_key_reused. A request that failed recorded nothing, so its key can be sent again. Keys do not expire: mint a new one for each new object. The Idempotency-Key on /finalize-payment is required as a valid UUID but is not used for replay; the payment's state machine itself prevents double-finalize. POST /payouts is special: idempotency lives in the body field idempotencyKey, not the header (see Payouts).
Generating keys
Use a UUID v4 from any standard library:
key=$(uuidgen)
When to mint a new key vs. reuse
- One operation → one key. Generate the key when you start the operation; persist it alongside the order so retries reuse it.
- A retry of the same operation reuses the same key. That's the whole point.
- A new attempt after a definitive failure uses a new key. If the original returned
400 invalid amount, that key is now permanently associated with that error — fix the input and use a fresh key.
// Pseudocode for resilient charge logic
async function charge(order) {
if (!order.idempotencyKey) {
order.idempotencyKey = randomUUID();
await db.orders.update(order.id, { idempotencyKey: order.idempotencyKey });
}
for (let attempt = 1; attempt <= 3; attempt++) {
try {
return await fynex.initializePayment({ ...order, key: order.idempotencyKey });
} catch (err) {
if (!isRetryable(err)) throw err;
await sleep(2 ** attempt * 100); // 200ms, 400ms, 800ms
}
}
}
Retry strategy
Retry on:
- Network errors (
ECONNRESET, timeouts, DNS failures) - HTTP
502 Bad Gateway(upstream provider hiccup) - HTTP
503 Service Unavailable
Don't retry on:
400 Bad Request— fix the payload401 Unauthorized— fix the token403 Forbidden— fix the permission/seller404 Not Found— fix the resource ID409 Conflict— read the body and decide; usually a state issue, not transient
Use exponential backoff with jitter: 200ms → 400ms → 800ms with random jitter, capped at 3–5 attempts.
async function retry(fn, max = 4) {
for (let i = 0; i < max; i++) {
try { return await fn(); }
catch (e) {
if (!isRetryable(e) || i === max - 1) throw e;
const base = Math.min(2 ** i * 200, 5000);
const jitter = Math.random() * base * 0.3;
await new Promise(r => setTimeout(r, base + jitter));
}
}
}
Caution
Don't retry forever. If three retries fail, surface the error to your operator and stop. A stuck payment with a known idempotency key can be inspected and resolved manually.
What gets replayed
Fynex binds the key to the original operation and returns the existing resource instead of creating another one. On POST /initialize-payment, a replay returns 200 OK. For an active APM, Fynex re-reads the existing provider charge and rehydrates its current buyer action: actionUrl / redirectFullPage for redirects and paymentInstructions for a Multibanco payment reference. The payment's canonical status can still advance asynchronously through webhooks or polling.
Reusing a key with a different body
POST /initialize-payment enforces strict idempotency on the high-stakes financial fields of the request: amount, currencyCode, countryCode, externalOrderRef, paymentType, paymentMethod. If any of those values differ from the original request that minted the key, the API returns 409 Conflict with a concrete reason naming the field and both values:
{ "error": "Idempotency-Key reused with a different currencyCode: original=USD, request=EUR" }
{ "error": "Idempotency-Key reused with a different amount: original=4999 minor units, request=9999 minor units" }
This matters: a silent replay of the original response on a body mismatch could cause an integrator to charge a customer an amount or currency they did not intend.
Fields that are not part of the conflict check (e.g. billingDetails.addressLine2, cardData.cvv, customer profile metadata, headers like X-Device-Fingerprint) can vary between retries, but the replay still refers to the original payment; it does not re-run provider creation with the changed ancillary data. The intent is "same financial transaction is safe to look up again; different financial transaction requires a fresh key."
When you see a 409 from this check:
- Decide whether you actually want to replay the original payment or create a new one.
- To replay → re-send the request with the original body unchanged.
- To create a new payment → mint a fresh
Idempotency-Key(fresh UUID) and send the new body.
Errors and replays
Error replay behaviour is endpoint-specific and not guaranteed across all operations. For safe recovery from a validation error, fix your input and use a new idempotency key regardless of whether the original error was replayed.
Next steps
- Errors — distinguish retryable from permanent failures
- Polling & SSE — poll for the canonical payment state