Erros
Toda resposta 4xx/5xx carrega o mesmo envelope JSON:
{
"code": 400,
"message": "invalid_input",
"details": [
{ "field": "items[0].unit_price", "message": "must be a positive decimal string" }
]
}code— o status HTTP numérico (400,401,404, …). Útil para tratamento genérico na camada HTTP, mas para lógica de ramificação faça switch emmessageem vez disso —codenão vai desambiguar entre, digamos,invalid_inputepayment_method_not_supported(ambos 400).message— o nome sentinela em lower-snake-case, derivado da constante interna emerrors.go(por exemplo,INVALID_INPUT → "invalid_input"). Estável entre releases — faça switch nisso.details— preenchido em erros de validação. Array de objetos{ field, message }para que o cliente possa amarrar erros a inputs. Omitido caso contrário.
No momento a gente não retorna trace_id nem timestamp no body.
Rascunhos anteriores desta página prometeram os dois — isso era
aspiracional. Se você precisa correlacionar um log do servidor a
uma requisição, capture o header Date da resposta e os headers
de rate-limit do lado do gateway (X-RateLimit-*) e cite isso em
um ticket de suporte.
Rejeições na borda do gateway usam um formato diferente. O
envelope acima é o que os serviços de backend emitem. Requisições
rejeitadas no gateway antes de chegar a um serviço — um
X-Signature ausente/inválido, um X-Client-ID desconhecido ou
um X-Timestamp velho numa chamada /b2b/v1/* — voltam como
{ "error": "...", "message": "..." }, onde error é um slug
grosseiro (unauthorized / bad_request / service_unavailable)
e message carrega as especificidades. Não há code numérico
nem details. Então um verificador deve ramificar pelo status
HTTP primeiro, depois ler message, e só tratar code/details
como presentes uma vez que a requisição passou pelo gateway.
Exemplo de body do gateway (401):
{ "error": "unauthorized", "message": "invalid signature" }Status HTTP → códigos típicos
| HTTP | Valores típicos de code | O que significa |
|---|---|---|
| 400 | INVALID_INPUT, MISSING_REQUIRED, INVALID_FORMAT, INVALID_LENGTH, INVALID_VALUE, INVALID_USER_STATUS, INVALID_USER_ROLE, PAYMENT_METHOD_NOT_SUPPORTED, AMOUNT_BELOW_MINIMUM | Requisição ruim — olhe details |
| 401 | INVALID_CREDENTIALS, INVALID_TOKEN, TOKEN_EXPIRED, INVALID_SIGNATURE (B2B); INVALID_OTP, OTP_EXPIRED, SESSION_EXPIRED (só dashboard / JWT) | Auth falhou — chave ruim, timestamp expirado, assinatura errada. Os códigos OTP/SESSION_EXPIRED só aparecem em rotas de dashboard-JWT (/payment/v1/*); integrações B2B puras não vão ver. |
| 402 | INSUFFICIENT_CREDIT | Saldo pré-pago do lojista acabou — recarregue antes de retentar |
| 403 | FORBIDDEN, IP_BLOCKED | Chave é válida, mas não tem o escopo/permissão de IP para esta chamada |
| 404 | NOT_FOUND, RECORD_NOT_FOUND, USER_NOT_FOUND, SESSION_NOT_FOUND | Recurso não existe (ou não existe para este lojista) |
| 409 | ALREADY_EXISTS, USER_ALREADY_EXISTS, SESSION_ALREADY_EXISTS | Replay de idempotência com body diferente, ou a máquina de estados recusou a transição |
| 429 | TOO_MANY_REQUESTS, TOO_MANY_ATTEMPTS | Rate limit batido; backoff e retente |
| 500 | INTERNAL_SERVER_ERROR, DATABASE_CONNECTION_ERROR, DATABASE_QUERY_ERROR, DATABASE_TRANSACTION_ERROR, REDIS_CONNECTION_ERROR, REDIS_OPERATION_ERROR, EXTERNAL_SERVICE_ERROR | Erro nosso; seguro retentar com backoff. (Códigos específicos de provedor como TWILIO_SERVICE_ERROR / SENDGRID_SERVICE_ERROR existem internamente mas só aparecem em fluxos de notificação do lado do dashboard, não em endpoints B2B.) |
| 503 | SERVICE_UNAVAILABLE | Uma dependência downstream caiu. Retente com backoff |
Referência completa de códigos
O conjunto completo de valores de code que você pode ver (bate
com payment-service/pkg/errors/errors.go):
Auth e sessões (401)
INVALID_CREDENTIALS— combinação de usuário/senha ou chave de API rejeitadaINVALID_TOKEN— JWT/session token não-parseável ou adulteradoTOKEN_EXPIRED— JWT além deexpINVALID_OTP— OTP não combinaOTP_EXPIRED— OTP emitido há mais que a janela de tolerânciaSESSION_EXPIRED— sessão do dashboard expirouINVALID_SIGNATURE— assinatura HMAC não bate em chamadas B2B / webhook
Autorização (403)
FORBIDDEN— autenticado mas a fronteira de role/escopo/lojista bloqueia a açãoIP_BLOCKED— IP está na lista de abuso
Não encontrado (404)
NOT_FOUND— genéricoRECORD_NOT_FOUND— linha ausente para o ID dadoUSER_NOT_FOUND— busca de usuário falhouSESSION_NOT_FOUND— ID de sessão do dashboard não reconhecido
Conflito (409)
ALREADY_EXISTS— genéricoUSER_ALREADY_EXISTS— cadastro bateu numa constraint de unicidadeSESSION_ALREADY_EXISTS— insert de sessão duplicada
Validação (400)
INVALID_INPUT— genérico; chequedetailsMISSING_REQUIRED— um campo obrigatório estava ausenteINVALID_FORMAT— valor não combinou com o formato esperado (por exemplo, UUID, URL, e-mail)INVALID_LENGTH— valor muito curto ou muito longoINVALID_VALUE— valor fora do enum/range permitidoINVALID_USER_STATUS— usuário está num estado que não permite a açãoINVALID_USER_ROLE— role não tem permissão para a açãoPAYMENT_METHOD_NOT_SUPPORTED— combinação provider/ativo não está habilitada para o lojistaAMOUNT_BELOW_MINIMUM— valor do pedido abaixo do piso por rede ou por ambiente. A resposta trazdetails.floor_usdcom o piso configurado (em USD), então você pode exibi-lo diretamente; o texto demessagetambém o informa.INSUFFICIENT_BALANCE— a carteira do comprador não tem saldo suficiente do ativo de pagamento para cobrir a transferência.INSUFFICIENT_GAS— a carteira do comprador não tem gas nativo suficiente para transmitir a transferência.
Pagamento / faturamento (402)
INSUFFICIENT_CREDIT— saldo pré-pago do lojista não consegue cobrir o gas / taxa de plataforma. Recarregue pelo dashboard, depois retente
Rate limiting (429)
TOO_MANY_REQUESTS— rate-limit de IP no gatewayTOO_MANY_ATTEMPTS— tentativas falhadas repetidas no mesmo recurso (por exemplo, OTP) dispararam um throttle
Armazenamento / infraestrutura (500)
DATABASE_CONNECTION_ERROR— não conseguiu alcançar o bancoDATABASE_QUERY_ERROR— plano de query falhou em runtimeDATABASE_TRANSACTION_ERROR— commit/rollback falhouREDIS_CONNECTION_ERROR— não conseguiu alcançar o RedisREDIS_OPERATION_ERROR— comando Redis falhouEXTERNAL_SERVICE_ERROR— falha genérica de terceiro (provedor não categorizado abaixo)TWILIO_SERVICE_ERROR— chamada de SMS / Verify do Twilio falhouSENDGRID_SERVICE_ERROR— envio de e-mail do SendGrid falhouINTERNAL_SERVER_ERROR— fall-through inesperado; capture o headerDateda resposta +X-RateLimit-*e fale com a gente
Disponibilidade (503)
SERVICE_UNAVAILABLE— um downstream crítico está reportando como não saudável; backoff + retry
Erros de validação (400)
Quando o problema é um body de requisição malformado, details é
um array para que você possa mapear erros de volta para campos:
{
"code": 400,
"message": "invalid_input",
"details": [
{ "field": "items[0].unit_price", "message": "must be a positive decimal string" },
{ "field": "success_url", "message": "must be a valid https URL" }
]
}Erros de auth (401)
INVALID_SIGNATURE cobre três modos distintos de falha — a única
forma de distinguir é por bissecção:
- Chave secreta errada (re-cheque a env var)
- Defasagem de timestamp > 5 min (sincronize NTP)
- String canônica errada (mais comum: esqueceu os separadores
\nou assinou um body parseado-e-re-stringificado que difere do que você mandou)
Veja Autenticação para o algoritmo exato de assinatura.
Rate limits (429)
| Superfície | Limite |
|---|---|
Toda rota de gateway (incl. /b2b/v1/*) | Por IP no gateway — 500 req/min, bucket compartilhado. Não por lojista. |
Checkout público (/checkout/*) | Sub-bucket mais estrito — 20 req/min por IP |
X-RateLimit-Limit e X-RateLimit-Remaining são emitidos em
toda resposta com rate-limit do gateway (não só nas de
sucesso). Trate como o orçamento vivo para o seu IP.
Respostas com rate-limit incluem um header Retry-After (segundos
até a janela resetar) — honre-o. Como fallback, backoff com jitter —
1s base + exponencial até 30s.
Rate limits estão sujeitos a mudança. Se você está batendo neles legitimamente (por exemplo, reconciliando uma faixa histórica grande), fale com a gente — endpoints de bulk estão no roadmap.
Crédito insuficiente (402)
INSUFFICIENT_CREDIT (HTTP 402 Payment Required) significa que
o saldo pré-pago do lojista não consegue cobrir o próximo
gas-e-taxa-de-plataforma para a operação que você tentou — tipicamente
liquidar um pagamento cripto ou executar uma ação on-chain
patrocinada por gas. Recarregue pelo dashboard do lojista
(Billing → Add credit), depois retente a operação; trabalho em
voo espera e segue automaticamente quando o saldo libera.
Erros de servidor (5xx)
Um 500 significa que a gente não conseguiu processar a requisição.
Retente com backoff — a sua idempotency_key garante que você não
vai cobrar em dobro se a requisição original parcialmente teve
sucesso.
Se as retentativas não recuperam em um minuto, exiba um genérico
“pagamento temporariamente indisponível” para o comprador e fale
com o suporte com o endpoint que falhou, o ID do seu lojista, o
header Date da resposta e os valores X-RateLimit-*, e o horário
aproximado da requisição — isso é o suficiente para a gente girar
para a requisição relevante nos nossos logs.
Erros de entrega de webhook
Entregas de webhook são um canal de falha separado — não aparecem como erros de API porque não é o seu servidor que está chamando. Quando uma entrega retorna não-2xx (ou dá timeout), ela é enfileirada para retry com backoff exponencial em 0s, 1min, 5min, 15min, 1h, 6h (seis tentativas no total — mesmo cronograma de Webhooks → Visão geral). O histórico completo por entrega aparece em Developers → Webhooks → [endpoint] → Delivery log no dashboard. Depois da sexta tentativa a entrega vai para dead-letter — o dashboard mostra um badge “Failed” que você pode replayar manualmente assim que o seu servidor estiver saudável.