Idempotency
Make your POSTs and PATCHes safe to retry — no duplicate users, no double-charges, even when the network drops mid-call.
Idempotency
An idempotency key is like a sticky note attached to your request. If your network drops in the middle of a POST /v1/transactions and you don't know whether the transaction was created, you retry with the same key — and BlendFi returns the same response instead of creating a second transaction. No duplicates. No double-charges.
Every POST and PATCH in the BlendFi API requires an Idempotency-Key header. This guide tells you how to use it correctly.
At a glance
One key per logical operation
Generate a fresh UUID per intent (one quote, one user creation, one transaction). Reuse the same key across retries of that intent.
Retry with the same key, get the same response
Same key + same body returns the original response — exact status, exact JSON. The operation is never executed twice.
Different body with the same key is rejected
If you accidentally reuse a key for a different request, you get `409 idempotency_key_reused`. This protects you from your own bugs.
How it works
1. Generate a key per intent
Mint a unique string before you send the request. UUID v4 is the recommended format — anything random and unpredictable works.
import { randomUUID } from "node:crypto";
const idempotencyKey = randomUUID();
// "0196c5d9-2e34-7c24-a47e-a0e1f89bb8a9"The key represents one logical operation, not one network attempt. If you retry, reuse the same key.
2. Send the key on every POST and PATCH
Add it to every mutating request as the Idempotency-Key header:
curl -X POST $BLENDFI_BASE/v1/transactions \
-H "Authorization: Bearer $BLENDFI_KEY" \
-H "Idempotency-Key: 0196c5d9-2e34-7c24-a47e-a0e1f89bb8a9" \
-H "content-type: application/json" \
-d '{"quote_id": "01J..."}'If you forget the header on a POST or PATCH, BlendFi rejects with 400 idempotency_key_required. GET, DELETE, and other read-only methods don't need it.
3. Retry with the same key on failure
When a request times out, the connection drops, or you get a 5xx — retry with the same key and same body. BlendFi will:
- If the original request succeeded, return the same response (same status, same body) without re-executing.
- If the original request is still in flight (less than 60 seconds old), return
409 idempotency_key_in_progress. Wait and retry. - If the original request failed with a
5xx, re-execute the operation.5xxresults are never cached, because the failure means we don't know whether the operation actually happened.
async function createTransaction(quoteId, key) {
for (let attempt = 0; attempt < 4; attempt++) {
const res = await fetch(`${BASE}/v1/transactions`, {
method: "POST",
headers: {
Authorization: `Bearer ${KEY}`,
"Idempotency-Key": key,
"content-type": "application/json",
},
body: JSON.stringify({ quote_id: quoteId }),
});
if (res.status >= 500) {
await sleep(2 ** attempt * 250);
continue;
}
return res.json();
}
throw new Error("transaction failed after retries");
}What stays the same across retries
When BlendFi replays a stored response, everything matches the original:
| First call | Retried call (same key) | |
|---|---|---|
| HTTP status | 201 Created | 201 Created |
| Response body | {"id":"01J...","status":"pending",...} | {"id":"01J...","status":"pending",...} (byte-for-byte) |
| Side effects | Transaction created, audit event emitted | None — no second transaction, no second audit event |
| Response headers | Original | Original (minus content-length, recomputed) |
This means your retry logic can treat a replayed response identically to the original. You don't need to detect "this is a replay" — just parse it.
Retry rules at a glance
The 4xx vs 5xx rule
Errors in the 4xx range (validation failures, conflicts, missing capabilities) are cached. Retrying with the same key will return the same 4xx — fix your request and use a new key.
Errors in the 5xx range (BlendFi-side failures) are not cached. Retrying with the same key re-executes the operation. This is intentional: a 5xx means we don't know if the operation succeeded, so we keep the key live for retries.
| Original outcome | What happens on retry with same key |
|---|---|
2xx success | Returns cached response. No re-execution. |
4xx client error | Returns cached error. Use a new key after fixing. |
5xx server error | Re-executes. Retry is safe. |
| Still in flight (under 60 s) | Returns 409 idempotency_key_in_progress. Wait. |
| Different body, same key | Returns 409 idempotency_key_reused. Bug — investigate. |
Common pitfalls
Reusing one key across multiple operations.
A key is tied to one logical request — one POST /v1/users, one POST /v1/transactions, etc. If you reuse a key from a user creation for a transaction creation, you get 409 idempotency_key_reused because the body differs.
Generating a new key per attempt instead of per intent.
If your retry loop calls randomUUID() inside the loop, every retry gets a different key, and BlendFi treats each as a fresh request. You will create duplicates. Generate the key once, outside the retry loop.
Discarding keys before retrying long-running flows. Sandbox keeps idempotency records for 24 hours. If your retry happens more than 24 hours later, the key has expired and the request executes fresh. For one-shot flows (user creation, transaction execution) this is fine. For multi-day workflows, persist the response after the first successful call so you don't need to retry across the expiration boundary.
Treating in-flight conflicts as a fatal error.
409 idempotency_key_in_progress means an earlier attempt is still running. Wait a second and retry — don't error out.
When you don't need it
Read-only requests (GET, HEAD, OPTIONS) don't need an idempotency key. They have no side effects to deduplicate. The header is rejected on these methods only if you explicitly pass it with conflicting values.
DELETE is naturally idempotent — deleting an already-deleted resource returns 404, not a duplicate delete. We don't require an idempotency key on DELETE for that reason.
What to read next
Errors and retries
The full retry policy by error code — which `code` values are safe to retry, with what backoff.
Transaction lifecycle
See idempotency in context: how retries on each lifecycle step affect transaction state.
API reference
Every POST and PATCH endpoint — every one of them takes an Idempotency-Key header.
FAQ
What format does the key need to be? Any string between 1 and 255 characters. UUID v4 is recommended. Don't use sequential integers — collisions across systems become a real risk.
How long does BlendFi remember a key? 24 hours from first use, per organization. After that, the key is forgotten and a fresh request executes if you retry.
Can two different organizations use the same key?
Yes. Idempotency records are scoped per organization, so partner A's idempotency-key: 1234 is independent from partner B's. There is no cross-tenant collision risk.
What if my retry hits a different BlendFi region? BlendFi's idempotency store is centralized — all regions consult the same record set. You won't get duplicate execution from regional failover.
Does the idempotency check verify the request body byte-for-byte?
We hash method + path + body with SHA-256 and compare. Any change to any of those three triggers 409 idempotency_key_reused. Whitespace and key ordering in JSON matter — if you serialize differently on retry, you'll trip the check.
What if I want to force a re-execution after a previous request succeeded? Use a new key. There is no API to invalidate a stored response.
