Erros
Todo erro da Moonip é um objeto JSON com formato estável — programe seu handler contra ele uma vez e pare de adivinhar. Toda resposta, de sucesso ou de erro, também traz o cabeçalho X-Request-Id (gerado quando você não manda um, devolvido igual quando você manda).
#Formato
#Criação com resultado ainda desconhecido
409 idempotency_outcome_unknown significa que a criação original pode ter chegado ao provedor, mas o resultado ainda não foi confirmado. Repita com a mesma Idempotency-Key e o mesmo corpo, aumentando o intervalo entre tentativas. Não gere outra chave para essa cobrança: isso poderia criar outro pagamento.
A operação original permanece reservada mesmo após cinco minutos ou 24 horas. A repetição recupera o resultado salvo quando disponível; caso contrário, retorna esse erro sem criar outra cobrança. Uma consulta sem resultado não prova que a cobrança não foi criada. Se a situação persistir, contate o suporte com o request_id. Respostas concluídas ficam disponíveis por pelo menos 24 horas.
#Estrutura da resposta
{
"error": {
"type": "invalid_request_error",
"code": "invalid_request",
"message": "Validation failed",
"details": [
{ "param": "amount", "message": "must be a positive integer" }
]
},
"request_id": "req_01J..."
}| Campo | Sempre presente | Descrição |
|---|---|---|
error.type | sim | Categoria estável — uma de invalid_request_error, authentication_error, permission_error, not_found_error, conflict_error, rate_limit_error, api_error. |
error.code | sim | Código de máquina estável, em snake_case. É nele que seu handler deve ramificar, não na message. |
error.message | sim | Explicação para humano. Pode logar; não é contrato para automação. |
error.param | quando exatamente um campo falha | Identifica esse campo. Com vários campos falhando, ele está ausente e só o error.details os lista. |
error.details | em falha de validação | Lista { param, message }[] para erro em vários campos. |
request_id | sim | Cite ao falar com o suporte — ele rastreia todo log, span e mensagem Kafka. Também vem no cabeçalho X-Request-Id. |
Por que
codee nãomessage: a mensagem é escrita para gente ler e muda sem aviso — inclusive de idioma. Ocodeé contrato. Umif (e.message === "Invalid API key")quebra na primeira vez que alguém melhorar a frase.
Apelidos de compatibilidade: o corpo da resposta também traz alguns campos planos no nível superior —
statusCode,message,requestIde (quando existem)code/fieldErrors— ao lado do objetoerror, para clientes (incluindo o painel) que os leem direto. Leia o objetoerroraninhado; os apelidos planos não têm garantia de sobreviver a uma versão major futura.
#Mapa de status
| HTTP | error.type | O que o cliente deve fazer |
|---|---|---|
| 200 | — | — |
| 201 | — | Guarde o id devolvido e siga. |
| 400 | invalid_request_error | Corrija o corpo/cabeçalhos e tente de novo. Não repita às cegas. |
| 401 | authentication_error | Confira se o cabeçalho Authorization: Bearer leva uma chave correta e ativa. |
| 403 | permission_error | Bloqueado por escopo, pela guarda antifraude/velocidade, por uma tentativa de reembolso com chave de API (marcador de mensagem REFUND_ADMIN_ONLY) ou pela allowlist de IP. Revise a requisição ou fale com o suporte. |
| 404 | not_found_error | Confira o id que você mandou. |
| 409 | conflict_error | Mesma Idempotency-Key com corpo diferente (idempotency_key_reused), primeira tentativa ainda em processamento (idempotency_key_in_flight) ou estado não cancelável (payment_not_cancellable). |
| 422 | invalid_request_error | Violação de regra de negócio — leia a message, corrija a entrada, não repita. |
| 429 | rate_limit_error | Reduza o ritmo. Repita com atraso exponencial + jitter; respeite o Retry-After quando vier. |
| 5xx | api_error | Seguro repetir com a mesma Idempotency-Key — a idempotência é o que te protege de duplicar. |
#Erros comuns
#400 Bad Request
Validação falhou — o error.code é invalid_request e o error.details lista cada campo que falhou (error.param só vem quando exatamente um campo falha).
{
"error": {
"type": "invalid_request_error",
"code": "invalid_request",
"message": "Validation failed",
"details": [
{ "param": "amount", "message": "must be a positive integer" },
{ "param": "payment_method", "message": "Invalid enum value" }
]
},
"request_id": "req_01J..."
}Também dá 400 quando falta o cabeçalho Idempotency-Key numa escrita financeira. Em POST /v1/charges e POST /v1/pix/charges o cabeçalho é o único canal. Em POST /payments o campo idempotencyKey no corpo é aceito como apelido — se ambos forem enviados, precisam coincidir (divergência é 400) e o valor do cabeçalho é o que vale. POST /payments/{id}/refund é exclusivo do painel administrativo, então nunca recebe nenhuma das duas formas de uma chave de API.
Ação Corrija a requisição e tente de novo. Nunca em laço.
#401 Unauthorized
{
"error": {
"type": "authentication_error",
"code": "unauthorized",
"message": "Invalid API key"
},
"request_id": "req_01J..."
}Causas possíveis:
- Cabeçalho ausente — confira se o
Authorization: Bearer ak_live_...está sendo enviado. - A chave foi girada — veja no painel.
- Espaço em branco — a chave é comparada exatamente, sem espaço na frente ou atrás.
Ação Verifique a chave. Se ela estiver certa, pode ter sido desativada — fale com o suporte.
#403 Forbidden
A guarda de fraude/velocidade barra a cobrança antes de ela chegar no provedor — o error.code começa com fraud.:
{
"error": {
"type": "permission_error",
"code": "fraud.customer_velocity_exceeded",
"message": "Too many payment attempts for this customer in a short window"
},
"request_id": "req_01J..."
}Existe um segundo código fraud.* para o limite de velocidade da própria conta.
Ação Revise o cliente ou reduza o ritmo das tentativas. Só cobranças vivas contam no limite — cobranças expiradas ou canceladas (ex.: um QR Pix que venceu sem ser pago) são ignoradas, então tentar de novo nesses casos é seguro. Se o bloqueio parecer errado, escreva para suporte@moonip.io com o request_id.
#Reembolsos — marcador de mensagem REFUND_ADMIN_ONLY
POST /payments/{id}/refund recusa toda chamada com chave de API com 403, seja qual for o corpo: reembolsos são emitidos apenas no painel administrativo da Moonip. Isso não é um valor de code — o error.code é o "forbidden" comum e o token sobrevive só como prefixo da mensagem, então ramifique pelo código para a recusa e combine o prefixo apenas como marcador (ex.: message.startsWith("REFUND_ADMIN_ONLY")):
{
"error": {
"type": "permission_error",
"code": "forbidden",
"message": "REFUND_ADMIN_ONLY: reembolsos devem ser solicitados pelo painel/admin, ..."
},
"request_id": "req_01J..."
}Veja Referência da API — Reembolsos.
#Allowlist de IP
Quando a conta tem uma allowlist de IP configurada, uma requisição vinda de um endereço fora dela é recusada com 403 — o mesmo envelope permission_error, com error.code "forbidden". A checagem é uma guarda por conta nas rotas centrais da conta (carteiras, saques, transações, casos MED, api-keys e vizinhas); as rotas de movimentação de dinheiro dos serviços de pagamento e webhooks (/v1/charges, /v1/pix/charges, /v1/payments, /v1/webhooks) não são cobertas por ela. Coloque na allowlist todo endereço de saída das suas integrações (incluindo CI e hosts de failover), ou a requisição nem chega à checagem da chave. Detalhes e como atualizar a lista: Chaves de API.
#account_suspended
A conta está suspensa ou bloqueada: as requisições com chave de API a cobranças, pagamentos, saques, carteira e transações são recusadas até o suporte liberar a conta. A chave continua válida — trocar de chave não resolve.
{
"error": {
"type": "permission_error",
"code": "account_suspended",
"message": "Conta suspensa: a API não aceita requisições desta conta. Fale com o suporte."
},
"request_id": "req_01J..."
}Ação Não tente de novo. Escreva para suporte@moonip.io com o request_id.
#404 Not Found
{
"error": {
"type": "not_found_error",
"code": "not_found",
"message": "Charge ch_a1b2c3d4-0000-0000-0000-000000000009 not found"
},
"request_id": "req_01J..."
}Ação Confira o id. Se você acabou de criar o recurso, espere 1 a 2 segundos e tente de novo.
#409 Conflict
{
"error": {
"type": "conflict_error",
"code": "idempotency_key_reused",
"message": "Idempotency key 'ORD-7821' already used with a different request body"
},
"request_id": "req_01J..."
}Você reenviou uma Idempotency-Key com corpo diferente do original. A Moonip recusa sobrescrever em silêncio.
Dois outros códigos de conflict_error caem em 409:
error.code | Quando | Ação |
|---|---|---|
idempotency_key_reused | Mesma chave, corpo diferente. | Use uma chave nova — causa comum é reaproveitar o id do pedido depois que o cliente mexeu no carrinho. |
idempotency_key_in_flight | A primeira requisição com essa chave ainda está em processamento. | Espere ela terminar (ou repita depois de alguns segundos) — não troque de chave. |
payment_not_cancellable | POST /payments/{id}/cancel numa cobrança em PROCESSING. | Espere a cobrança assentar e então reembolse pelo painel administrativo. |
#Recusa por regra de negócio
Falha de formato (campo faltando ou inválido) é 400 com error.type: "invalid_request_error". Algumas falhas de configuração mais abaixo — por exemplo, nenhum provedor configurado para o payment_method e currency pedidos — vêm repassadas do orquestrador interno com o status dele e um código de máquina em UPPER_SNAKE_CASE, normalmente 422:
{
"error": {
"type": "invalid_request_error",
"code": "NO_PROVIDER_CONFIGURED",
"message": "No payment provider is configured for this payment method and currency."
},
"request_id": "req_01J..."
}Códigos dessa família do lado de cobrança: PROVIDER_UNAVAILABLE, PROVIDER_ERROR, PAYMENT_REJECTED, NO_PROVIDER_CONFIGURED, UNKNOWN_METHOD, MOCK_PROVIDER_IN_PRODUCTION, PAYER_IDENTITY_REQUIRED (quando expected_payer_tax_id trava a cobrança e o documento do pagador não bate).
Códigos do lado de saque/withdrawal: WALLET_NOT_FOUND, WALLET_NOT_SPENDABLE, AMOUNT_BELOW_MINIMUM, AMOUNT_ABOVE_MAXIMUM, DAILY_PAYOUT_LIMIT_EXCEEDED (422), INSUFFICIENT_BALANCE, WITHDRAWAL_NOT_CANCELLABLE. Note a exceção no caminho de saque: INSUFFICIENT_BALANCE chega como 400, não 422 — ramifique por error.code, não pelo status.
Ação Leia o error.code e a message. Não repita — corrija a entrada, ou fale com o suporte se a conta deveria ter provedor configurado.
#429 Too Many Requests
{
"error": {
"type": "rate_limit_error",
"code": "rate_limited",
"message": "Rate limit exceeded"
},
"request_id": "req_01J..."
}Cabeçalhos nessa resposta (veja os limites atuais em Primeiros passos):
Retry-After: 12
X-RateLimit-Limit-Minute: 5000
X-RateLimit-Remaining-Minute: 0Ação Reduza o ritmo — respeite o Retry-After quando vier, senão backoff exponencial com jitter.
async function comRetry(fn, tentativas = 5) {
for (let i = 0; i < tentativas; i++) {
try { return await fn(); }
catch (err) {
if (err.status !== 429 && err.status < 500) throw err;
const base = Math.min(1000 * 2 ** i, 30_000);
const jitter = Math.random() * 0.3 * base;
await new Promise(r => setTimeout(r, base + jitter));
}
}
throw new Error('tentativas esgotadas');
}#5xx — do lado do servidor
{
"error": {
"type": "api_error",
"code": "service_unavailable",
"message": "Upstream unavailable"
},
"request_id": "req_01J..."
}Seguro repetir. Use a mesma Idempotency-Key, para que uma cobrança duplicada nunca seja criada caso o processamento tenha dado certo e só a resposta tenha se perdido.
#Erros de entrega de webhook (servidor → seu endpoint)
Quando a Moonip não consegue alcançar o seu endpoint, a falha aparece em GET /v1/webhooks/deliveries:
{
"id": "b2c3d4e5-f6a7-4890-9abc-def012345678",
"status": "FAILED",
"attempts": 3,
"maxAttempts": 15,
"lastStatusCode": 502,
"lastError": "HTTP 502",
"lastResponseBody": "Bad Gateway",
"nextRetryAt": "2026-07-23T16:08:00.000Z"
}lastStatusCode / lastError | O que significa |
|---|---|
2xx | Entregue. Não retenta. |
4xx | Seu endpoint recusou o payload. A Moonip ainda retenta até maxAttempts — corrija e reenvie se precisar. |
5xx | Seu endpoint está fora. A Moonip retenta com backoff exponencial. |
429 / Retry-After | A Moonip respeita o seu Retry-After e reagenda. |
Connection timeout / ETIMEDOUT | Seu endpoint levou mais de 30s. Responda 200 OK rápido e processe em fila. |
getaddrinfo ENOTFOUND | Seu domínio não resolve. Atualize a URL registrada. |
self signed certificate | Erro de TLS. A Moonip exige certificado público válido. |
Depois de maxAttempts (padrão 15), a entrega vai para a DLQ e pode ser reenviada manualmente. Veja Webhooks para a semântica completa.
#Erros de gestão de webhook
Códigos nas rotas de administração de /v1/webhooks (permission_error / conflict_error / rate_limit_error conforme o código):
error.code | Quando |
|---|---|
WEBHOOK_ENDPOINT_DUPLICATE | A URL já está registrada nesta conta (409). |
WEBHOOK_ENDPOINT_LIMIT | A conta atingiu o limite de endpoints ativos. |
WEBHOOK_HTTPS_REQUIRED | Uma URL que não é https:// fora do sandbox/staging. |
WEBHOOK_OWN_DOMAIN | A URL aponta para um domínio da Moonip. |
WEBHOOK_TEST_RATE_LIMIT | Chamadas demais a POST /endpoints/{id}/test em uma hora (429). |
WEBHOOK_ROLE_REQUIRED | O papel de quem chama não pode gerenciar endpoints (access.canManage: false). |
IMPERSONATION_VIEW_ONLY | A sessão está impersonando e pode só ler. |
TENANT_NOT_ACTIVE | A conta não está ativa. |
#Lendo o error.details
O error.details é uma lista plana de { param, message } — renderizável direto:
{error.details.map(({ param, message }) => (
<li key={param}>{param}: {message}</li>
))}O apelido fieldErrors é a mesma informação reformatada como { field: reasons[] }:
{
"fieldErrors": {
"amount": ["must be a positive integer"]
}
}#Quando escrever para o suporte
Fale com suporte@moonip.io se:
- Um 5xx persistir por mais de 5 minutos
- Uma entrega de webhook ficar presa em
FAILEDdepois domaxAttempts - Um
403 Forbiddenaparecer sem explicação - A mesma
Idempotency-Keydevolver cobranças diferentes em chamadas diferentes (isso é bug — não pode acontecer)
Sempre cite o request_id — ele nos deixa rastrear todo log, span e mensagem Kafka numa consulta só.
#Limite de consultas por entidade
HTTP 429, error.type: rate_limit_error, error.code: entity_rate_limited. Aguarde os segundos de Retry-After antes de consultar a mesma entidade novamente. Veja Consultas periódicas e limites.