Erros e retentativas
Cada código de erro mapeado para significado claro, quem causou, se deve retentar e com qual backoff.
Erros e retentativas
Quando algo falha na API da BlendFi, a resposta é sempre JSON e tem sempre o mesmo formato. Este guia mostra como ler, o que cada code significa e quais erros são seguros para retentar.
O envelope de erro
{
"code": "invalid_transaction_state",
"message": "Transaction cannot transition from 'buying_crypto' to 'completed'.",
"request_id": "01KPR9F6MM8G147177J7ZQPJHG",
"details": { /* opcional, presente em erros de validação */ }
}Decida pelo `code`
`code` é estável, legível por máquina e nunca muda de significado. Toda sua lógica de erro deve se basear nesse campo.
Mostre `message` para humanos
`message` é a descrição legível por humanos. A gente pode reescrever ao longo do tempo, então não faça `match` em cima do texto.
Envie `request_id` quando pedir ajuda
Toda resposta carrega um `request_id`. Inclua nos e-mails de suporte — encontramos sua requisição em segundos.
Como tratar um erro
1. Faça parse do código primeiro
Não confie só no status HTTP. O mesmo 400 pode ser validation_error (você corrige), invalid_json (também você) ou idempotency_key_required (você esqueceu o cabeçalho). Decida pelo code, não pelo status.
const res = await fetch(url, options);
if (!res.ok) {
const error = await res.json();
switch (error.code) {
case "validation_error":
// Mostra erros de campo a partir de error.details.issues
return showFieldErrors(error.details?.issues);
case "idempotency_key_required":
throw new Error("chave de idempotência ausente — bug na camada de retry");
case "rate_limit_exceeded":
return retryAfterBackoff(error);
case "internal_error":
return retryWithBackoff(error);
default:
return reportUnexpected(error);
}
}2. Decida se vai retentar
Use a política de retentativa abaixo. Versão curta: 5xx e 429 são sempre seguros para retentar com a mesma chave de idempotência. 4xx não — corrija a requisição e use uma chave nova.
3. Logue o request_id em tudo
Tendo a chamada dado certo ou errado, guarde o request_id junto com a operação no seu log de auditoria. Quando um parceiro reportar "a transação X tá presa", esse ID nos permite rastrear a requisição exata em segundos — sem ele, a gente fica caçando por timestamp e ID de conta, o que é lento e ambíguo.
log.info({
request_id: res.headers.get("x-request-id"),
transaction_id: body.id,
status: res.status,
}, "blendfi.transaction.created");Política de retentativa
Uma regra
Retente 5xx e 429. Não retente 4xx. Use a mesma chave de idempotência quando retentar; gere uma chave nova só quando você mudou a requisição para corrigir um 4xx.
| Faixa de status | Causa | Seguro retentar? | Como |
|---|---|---|---|
2xx | Sucesso | n/a | n/a |
400, 401, 403, 404, 409, 422 | Sua requisição | Não | Corrija a requisição, use uma chave de idempotência nova |
429 rate_limit_exceeded | Você está enviando rápido demais | Sim | Respeite o cabeçalho Retry-After; backoff exponencial se ausente |
500 internal_error | Bug do lado da BlendFi | Sim | Mesma chave; backoff exponencial (250ms, 500ms, 1s, 2s, 4s, desiste) |
502, 503, 504 | BlendFi ou provedor externo indisponível | Sim | Mesma chave; backoff exponencial |
Um loop de retentativa razoável
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: retentativas esgotadas", { cause: lastError });
}Encapsule fn() para que a mesma chave de idempotência seja reusada entre as tentativas — veja o guia de Idempotência para entender por quê.
Catálogo completo de erros
4xx — sua requisição
| Código | HTTP | Significado | Como resolver |
|---|---|---|---|
validation_error | 400 | Validação de schema falhou (corpo, query ou parâmetros) | Leia details.issues para mensagens por campo; corrija e reenvie com chave de idempotência nova |
invalid_json | 400 | Corpo não pôde ser interpretado como JSON | Confira vírgulas finais, aspas não escapadas, encoding |
invalid_cursor | 400 | Cursor de paginação está malformado ou expirou | Reinicie a listagem do começo |
idempotency_key_required | 400 | Cabeçalho Idempotency-Key ausente em POST ou PATCH | Adicione o cabeçalho; veja o guia de Idempotência |
authentication_required | 401 | Cabeçalho Authorization ausente ou malformado | Adicione Authorization: Bearer sk_test_… |
authentication_failed | 401 | Chave desconhecida, revogada ou com prefixo de ambiente errado | Copie a chave de novo; verifique sandbox vs produção |
unauthorized | 401 | Autenticado mas sem credenciais para esse recurso | Confira se o recurso pertence ao seu tenant |
missing_capability | 403 | Chave válida mas sem a capacidade para esse endpoint | Manda e-mail pra gente ampliar as capacidades da chave |
user_not_found | 404 | O user_id não existe ou está em outro tenant | Verifique o ID; confira se pertence à sua organização |
quote_not_found | 404 | O quote_id não existe ou está em outro tenant | Idem |
transaction_not_found | 404 | O transaction_id não existe ou está em outro tenant | Idem |
idempotency_key_reused | 409 | Essa Idempotency-Key foi usada com um corpo diferente | Bug no seu código — gere uma chave nova por operação lógica |
idempotency_key_in_progress | 409 | Uma requisição com a mesma chave ainda está rodando (até 60 s) | Espere um pouco e retente com a mesma chave |
invalid_transaction_state | 409 | Tentou avançar uma transação por uma transição de estado proibida | Re-busque a transação; respeite o ciclo de vida |
quote_already_consumed | 409 | Cotação já foi usada para criar uma transação | Crie uma cotação nova |
quote_expired | 409 | Cotação está mais velha que seu TTL (geralmente 30s) | Crie uma cotação nova |
duplicate_cpf | 409 | Outro usuário no seu tenant já tem esse CPF | Busque o usuário existente com GET /v1/users?cpf=… |
duplicate_external_id | 409 | Outro usuário no seu tenant já tem esse external_id | Idem |
429 — limite de taxa
| Código | HTTP | Significado | Como resolver |
|---|---|---|---|
rate_limit_exceeded | 429 | Você excedeu o limite de taxa por chave para esse endpoint | Respeite Retry-After; faça backoff; considere throttling no cliente |
5xx — problema nosso
| Código | HTTP | Significado | Como resolver |
|---|---|---|---|
internal_error | 500 | Falha inesperada do lado do servidor | Retente com a mesma chave de idempotência; se persistir, manda e-mail com o request_id |
upstream_unavailable | 502 | Um provedor externo (banco PIX, corretora, blockchain) está inacessível | Retente com backoff; sinalizamos o identificador do upstream em details.upstream |
Erros de validação em detalhe
Quando a resposta for 400 validation_error, details.issues lista cada campo com problema:
{
"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 é o caminho JSON do campo problemático. Mostre essas mensagens junto aos campos do seu formulário — seus usuários finais vão agradecer.
O que ler em seguida
Idempotência
O contrato de retentativa — mesma chave, mesma resposta. Obrigatório para toda estratégia de retry deste guia.
Ciclo de vida da transação
De onde vêm os erros `invalid_transaction_state` — as transições legais de estado, em um diagrama.
Ambientes e limites
Limites de taxa atuais por endpoint, diferenças sandbox vs produção, como pedir aumento.
FAQ
Devo retentar num 400 validation_error?
Não. A mesma requisição vai falhar do mesmo jeito toda vez. Corrija o corpo, gere uma chave de idempotência nova, envie de novo.
O campo code veio com um valor que eu nunca vi. O que faço?
Trate como internal_error para fins de retentativa (não retente operações destrutivas; retente leituras idempotentes com backoff). Manda e-mail pra gente com o request_id e a gente documenta o código ou conserta o bug.
Meu orçamento de retentativas acabou e eu não vi resposta final. Qual o estado?
Para POST e PATCH, busque o recurso direto com GET usando os parâmetros que você enviou. Se você usou os padrões recomendados de external_id/Idempotency-Key, seu dado é encontrável — você só não sabe se o create original deu certo. A busca te conta.
429 não veio com Retry-After. Quanto tempo eu espero?
Use backoff exponencial começando em 1 segundo, com teto de 30 segundos. A gente sempre tenta incluir Retry-After, mas respostas degradadas podem omitir.
Qual a diferença entre authentication_failed e unauthorized?
authentication_failed significa que a gente não consegue identificar você (chave ruim). unauthorized significa que a gente sabe quem você é mas você está tentando acessar um recurso que não é seu (por exemplo, uma transação de outra organização).
