Errors and retries
Error response shape, retry policy by HTTP category, and how to use request_id with support.
When something fails in the BlendFi API, the response is always JSON and always has the same shape. This page shows you how to read it, which errors are safe to retry, and how to use request_id with support. For the alphabetical reference of every code, see the Errors catalog.
The error response shape
{
"code": "kyc_required",
"message": "User KYC is not approved.",
"request_id": "01KPR9F6MM8G147177J7ZQPJHG",
"details": { /* optional, present in validation errors */ }
}Branch on `code`
`code` is stable, machine-readable, and never changes meaning. All your error logic should branch on this field.
Show `message` to humans
`message` is the human-readable description. We may rewrite it over time. Don't match on the text.
Send `request_id` when asking for help
Every response carries a `request_id`. Include it in support tickets, we find your call in seconds.
How to handle an error
1. Parse the code first
Don't decide on HTTP status alone. The same 400 may be validation_error (you fix), invalid_json (you also), or idempotency_key_required (you forgot the header). Branch on code.
const res = await fetch(url, options);
if (!res.ok) {
const error = await res.json();
switch (error.code) {
case "validation_error":
return showFieldErrors(error.details?.issues);
case "idempotency_key_required":
throw new Error("missing idempotency key, retry-layer bug");
case "rate_limit_exceeded":
return retryAfterBackoff(error);
case "internal_error":
return retryWithBackoff(error);
default:
return reportUnexpected(error);
}
}2. Decide whether to retry
Use the retry policy below. Short version: 5xx and 429 are always safe to retry with the same idempotency key. 4xx is not, fix the request and use a new key.
3. Log request_id everywhere
Whether the call succeeded or failed, store request_id (from the JSON body) alongside the operation in your log. When you report "transaction X is stuck", that ID lets us trace the exact request in seconds.
const body = await res.json();
log.info({
request_id: body.request_id,
transaction_id: body.id,
status: res.status,
}, "blendfi.transaction.created");Retry policy
The one-line rule
Retry 5xx and 429. Don't retry 4xx. Use the same idempotency key when retrying; only generate a new key once you've changed the request to fix a 4xx.
| Status range | Cause | Safe to retry? | How |
|---|---|---|---|
2xx | Success | n/a | n/a |
400, 401, 403, 404, 409, 422 | Your request | No | Fix and use a new idempotency key |
429 rate_limit_exceeded | You're sending too fast | Yes | Honor retry_after_seconds in details; exponential backoff if absent |
500 internal_error | BlendFi-side bug | Yes | Same key; exponential backoff (250 ms, 500 ms, 1 s, 2 s, 4 s, then give up) |
502, 503, 504 | BlendFi or external provider unavailable | Yes | Same key; exponential backoff |
A reasonable retry loop
async function withRetry(fn) {
const MAX_ATTEMPTS = 5;
let lastError;
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
try {
const res = await fn();
if (res.status < 500 && res.status !== 429) return res;
lastError = await res.json();
} catch (networkError) {
lastError = networkError;
}
const backoff = Math.min(2 ** attempt * 250, 4000);
const jitter = Math.random() * 100;
await sleep(backoff + jitter);
}
throw new Error("blendfi: retries exhausted", { cause: lastError });
}Wrap fn() so the same idempotency key is reused across attempts. See Idempotency.
Codes every integration must handle
The full list lives in the Errors catalog. The ones below show up most often and deserve dedicated handling:
| Code | HTTP | Where it shows up |
|---|---|---|
validation_error | 400 | Body, query, or path params failed validation. Detail in details.issues |
idempotency_key_required | 400 | Idempotency-Key missing on POST or PATCH |
idempotency_key_reused | 409 | Same key with different body. Your bug |
idempotency_key_in_progress | 409 | Previous attempt still running, wait |
authentication_failed | 401 | Key unknown, revoked, or wrong environment |
missing_capability | 403 | Valid key, no scope for this endpoint |
kyc_required | 422 | End customer's platform KYC is not approved |
quote_expired | 409 | Quote past validity (15 minutes for onramp) |
quote_already_consumed | 409 | Quote already consumed by a transaction or conversion |
invalid_transaction_state | 409 | Tried a forbidden state transition; reread current state |
rate_limit_exceeded | 429 | Exceeded per-key limit on this endpoint |
internal_error | 500 | Our side; safe to retry with backoff |
Codes ending in _provider_unavailable (kyc_provider_unavailable, pix_provider_unavailable, etc.) always indicate transient upstream failure and are 502. Safe to retry.
Validation errors in detail
When the response is 400 validation_error, details.issues lists each problematic field:
{
"code": "validation_error",
"message": "Request validation failed.",
"request_id": "01KPR9F6MM8G147177J7ZQPJHG",
"details": {
"issues": [
{ "path": "cpf", "message": "must be 11 digits" },
{ "path": "external_id", "message": "is required" }
]
}
}path is the JSON path of the offending field. Show those messages next to your form fields; the end customer thanks you.
FAQ
Should I retry on 400 validation_error?
No. The same request will fail the same way every time. Fix the body, generate a new key, resend.
The code came with a value I've never seen. What do I do?
Treat as internal_error for retry purposes: don't retry destructive operations; retry reads with backoff. Open a ticket with the request_id and we'll either document the code or fix the bug.
My retry budget is exhausted and I never saw a final response. What's the state?
For POST and PATCH, fetch the resource directly with GET using the identifiers you sent. If you used consistent external_id or Idempotency-Key, your data is findable; you just don't know if the original create succeeded, and the lookup resolves that.
429 came without retry_after_seconds. How long do I wait?
Use exponential backoff starting at 1 second, capped at 30 seconds. In degraded responses details may come incomplete.
What's the difference between authentication_failed and missing_capability?
authentication_failed (401) means we cannot identify you (bad key, revoked, wrong environment). missing_capability (403) means we know who you are, but your key has no scope for the endpoint. The first you fix by copying the right key; the second requires a commercial adjustment.
Next steps
- Errors catalog: the alphabetical list with filter, status, cause, recovery.
- Idempotency: the retry contract, same key, same response.
- Environments and rate limits: per-key limits and sandbox vs production differences.
