BlendFi

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 rangeCauseSafe to retry?How
2xxSuccessn/an/a
400, 401, 403, 404, 409, 422Your requestNoFix and use a new idempotency key
429 rate_limit_exceededYou're sending too fastYesHonor retry_after_seconds in details; exponential backoff if absent
500 internal_errorBlendFi-side bugYesSame key; exponential backoff (250 ms, 500 ms, 1 s, 2 s, 4 s, then give up)
502, 503, 504BlendFi or external provider unavailableYesSame 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:

CodeHTTPWhere it shows up
validation_error400Body, query, or path params failed validation. Detail in details.issues
idempotency_key_required400Idempotency-Key missing on POST or PATCH
idempotency_key_reused409Same key with different body. Your bug
idempotency_key_in_progress409Previous attempt still running, wait
authentication_failed401Key unknown, revoked, or wrong environment
missing_capability403Valid key, no scope for this endpoint
kyc_required422End customer's platform KYC is not approved
quote_expired409Quote past validity (15 minutes for onramp)
quote_already_consumed409Quote already consumed by a transaction or conversion
invalid_transaction_state409Tried a forbidden state transition; reread current state
rate_limit_exceeded429Exceeded per-key limit on this endpoint
internal_error500Our 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

On this page