Idempotency
Make your POSTs and PATCHes safe to retry. No duplicate users, no double-charges, even when the network drops mid-call.
An idempotency key uniquely identifies a logical request and lets the same operation be retried safely. If your network drops in the middle of a POST /v1/quotes/:id/accept and you don't know whether the transaction was created, retry with the same key: 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 page covers the contract and how to use it.
Three rules
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.
Same key on retry, same response
Same key + same body returns the original response, same status, same JSON. The operation never executes twice.
Different body with the same key is rejected
If you accidentally reuse a key across different requests, you get `409 idempotency_key_reused`. It guards against your own bugs.
How it works
1. Generate a key per intent
Create a unique string before sending the request. UUID v4 is the recommended format; any random unpredictable string works.
import { randomUUID } from "node:crypto";
const idempotencyKey = randomUUID();
// "0196c5d9-2e34-7c24-a47e-a0e1f89bb8a9"The key represents a logical operation, not a network attempt. If you need to retry, reuse the same key.
2. Send the key on every POST and PATCH
Add the Idempotency-Key header on every request that writes data:
curl -X POST $BLENDFI_BASE/v1/quotes/$QUOTE_ID/accept \
-H "Authorization: Bearer $BLENDFI_KEY" \
-H "Idempotency-Key: 0196c5d9-2e34-7c24-a47e-a0e1f89bb8a9" \
-H "content-type: application/json" \
-d '{"quote_id": "01J..."}'Forgetting the header on a POST or PATCH returns 400 idempotency_key_required. GET, DELETE, and other read-only methods do not require a key.
3. Retry with the same key on failure
When a request times out, the connection drops, or you receive 5xx, retry with the same key and same body. BlendFi:
- If the original request succeeded: returns the same response (same status, same body) without re-executing.
- If the original request is still in progress (under 60 seconds): returns
409 idempotency_key_in_progress. Wait and retry. - If the original request failed with
5xx: re-executes the operation.5xxresults are never cached; the failure means we don't know if the operation actually happened.
async function createTransaction(quoteId, key) {
for (let attempt = 0; attempt < 4; attempt++) {
const res = await fetch(`${BASE}/v1/quotes/${quoteId}/accept`, {
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("accept failed after retries");
}What stays identical across retries
When BlendFi replays a stored response, everything matches the original:
| First call | Replayed 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 event |
| Headers | Original | Original (except content-length, recomputed) |
Your retry logic can treat a replayed response as if it were the original. No need to detect "this is a replay"; just parse.
Retry rules in one table
The 4xx vs 5xx rule
4xx errors (validation, conflict, missing capability) are cached. Retrying with the same key returns the same 4xx; fix the request and use a new key.
5xx errors (BlendFi-side failures) are not cached. Retrying with the same key re-executes. This is intentional: a 5xx means we don't know if the operation succeeded, so we keep the key live for safe retries.
| Original outcome | What happens on retry with the same key |
|---|---|
2xx success | Returns the cached response. No re-execution. |
4xx client error | Returns the cached error. Use a new key after fixing. |
5xx server error | Re-executes. Retry is safe. |
| Still in progress (under 60 s) | Returns 409 idempotency_key_in_progress. Wait. |
| Different body, same key | Returns 409 idempotency_key_reused. Bug, investigate. |
Common pitfalls
Reusing a key across multiple operations.
A key is bound to a single logical request: one POST /v1/users, one POST /v1/quotes/:id/accept, etc. Reusing a user-creation key on a conversion creation returns 409 idempotency_key_reused because the body is different.
Generating a fresh key per attempt instead of per intent.
If your retry loop calls randomUUID() inside the loop, each retry gets a different key and BlendFi treats each as a new request. You'll create duplicates. Generate the key once, outside the loop.
Discarding keys before retrying in long-running flows. Idempotency records are kept for 24 hours from first use. If your retry happens more than 24 hours later, the key has expired and the request runs from scratch. For one-shot flows (user creation, transaction execution) this is not a problem. For multi-day flows, persist the response after the first successful call so you don't need to retry past expiration.
Treating in-progress conflict as fatal.
409 idempotency_key_in_progress means a previous attempt is still running. Wait a second and retry; do not treat it as fatal.
When you don't need it
GET,HEAD,OPTIONS: read-only, no side effect to deduplicate.DELETE: naturally idempotent; deleting an already-deleted resource returns404, not a duplicate delete.
For these methods, the Idempotency-Key header is optional. If sent, it is ignored.
FAQ
What format does the key need to follow? Any string between 1 and 255 characters. UUID v4 is recommended. Don't use sequential integers; cross-system collisions become a real risk.
How long does BlendFi keep a key? 24 hours from first use, per organization. After that, the key is discarded and a fresh request executes from scratch if you retry.
Can two different organizations use the same key?
Yes. Records are scoped per organization: Org A's idempotency-key: 1234 is independent of Org B's. No cross-tenant collision risk.
What if my retry hits a different BlendFi region? The idempotency store is centralized; every region queries the same record set. No duplicate execution from regional failover.
Does the check compare the body byte-for-byte?
We hash method + path + body with SHA-256 and compare. Any change in any of the three triggers 409 idempotency_key_reused. Whitespace and JSON key ordering matter: if you serialize differently on retry, you'll trip the check.
What if I want to force re-execution after a successful request? Use a new key. There is no API to invalidate a stored response.
Next steps
- Errors and retries: the full retry policy by error code, which ones are safe to retry, and with which backoff.
- Errors catalog: every
codeBlendFi can return, status, cause, recovery.
