Erros¶
Para: Dev — Conferido em 2026-10-18.
Todo erro da API /v1 é um objeto application/problem+json com um code estável. Esta página lista o formato do
erro, os 30 code que a API emite hoje, os 22 motivos de validação (errors[].reason), as respostas da borda que não
têm corpo e o que acontece quando uma dependência do Dracma está fora.
O que é¶
O corpo do erro segue a RFC 9457. O campo que o seu código testa é o code.
{
"type": "https://docs.dracma.test/errors/insufficient_scope",
"title": "insufficient_scope",
"status": 403,
"code": "insufficient_scope",
"trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
"retryable": false
}
| Campo | Presença | Uso |
|---|---|---|
code |
sempre | Estável. É o que o seu código testa. Códigos novos só se acrescentam. |
status |
sempre | O mesmo status HTTP da resposta. |
retryable |
sempre | true se vale repetir com a mesma Idempotency-Key. |
trace_id |
sempre | Também no cabeçalho Dracma-Trace-Id. Guarde no log; é o que o suporte pede. |
type |
sempre | Identificador opaco (https://docs.dracma.test/errors/<code>). Não é uma página: o domínio não resolve. |
title |
sempre | Igual ao code. |
detail |
omitido quando vazio | Aparece só em poucos erros (internal_error, day_open, ação de sandbox desconhecida). Nunca faça parse dele. |
errors[] |
em validation_failed |
Itens com field, message e reason (o reason é omitido se vazio). |
existing |
em 409 duplicate_external_id |
O depósito ou saque que já existe com esse external_id. |
O Dracma nunca devolve rastro de pilha, SQL, segredo, a assinatura esperada nem o pedido canônico calculado.
Quando usar¶
Use o code para decidir o fluxo do conector e o retryable (junto da classe HTTP) para decidir se repete. Use o
trace_id para falar com o suporte. Use errors[].field e errors[].reason para corrigir o seu pedido, e nunca para
mostrar texto ao jogador: message não é contrato.
Como funciona¶
O que fazer com cada resposta¶
| Resposta | Significado | Ação |
|---|---|---|
2xx |
Feito (ou já estava feito) | Grave o resultado. |
4xx, exceto 409 idempotency_in_progress, 409 day_open e 429 |
O Dracma decidiu recusar | Não repita igual. Corrija a causa ou encerre o fluxo. |
409 idempotency_in_progress, 409 day_open, 429, 5xx |
Sem efeito conclusivo, ou ainda cedo | Espere o Retry-After e repita com a mesma Idempotency-Key. |
| timeout, conexão caiu | Você não sabe se houve efeito | Repita com a mesma chave; ao desistir, consulte por external_id (Idempotência e repetição). |
code que você não conhece |
Código novo (a lista só cresce) | Trate pela classe HTTP e pelo retryable. |
Catálogo de code emitidos¶
São 30 códigos. Repetir = repetir com a mesma Idempotency-Key (retryable: true).
| HTTP | code |
Repetir | Quando | O que fazer |
|---|---|---|---|---|
| 400 | validation_failed |
não | Corpo, parâmetro ou cabeçalho inválido (errors[]); Idempotency-Key ausente; 4xx genérico do servidor HTTP |
Corrija o pedido. É erro do seu lado. |
| 400 | currency_not_enabled |
não | Moeda não habilitada na operação | Ajuste no onboarding; confira GET /currencies. |
| 400 | method_not_enabled |
não | Método não habilitado para a moeda (hoje só pix em BRL) |
Não ofereça o método. |
| 400 | rail_not_enabled |
não | POST /sandbox/bank-transfers/incoming: nenhum trilho bancário está habilitado |
Não use a rota. |
| 401 | invalid_signature |
não | Assinatura não confere, chave inexistente, nonce mal formado, cabeçalho ausente | Autenticação HMAC. |
| 401 | timestamp_out_of_window |
não | Fora de ±300 s, ou timestamp fora da forma canônica | Corrija o relógio; segundos. |
| 401 | replayed_nonce |
não | Nonce já usado com essa chave | Gere nonce novo a cada tentativa. |
| 401 | unknown_key |
não | key_id mal formado, de outro tipo de operação, ou chave revogada |
Confira a chave e o host. |
| 403 | ip_not_allowed |
não | IP de origem fora da lista da operação | A lista não é aplicada hoje; você não deve ver este código. |
| 403 | insufficient_scope |
não | A chave não tem o escopo da rota, ou o método não existe | Use a chave certa. |
| 403 | operation_suspended |
não | Operação suspensa pelo Dracma | Perguntas frequentes. |
| 404 | not_found |
não | O objeto não existe nesta operação (inclusive de outra operação, nunca 403); rota inexistente; GET em rota só POST |
Confira o id e a chave. |
| 409 | idempotency_key_reused |
não | Mesma chave com pedido diferente | Bug do seu lado. |
| 409 | idempotency_in_progress |
sim | A mesma chave ainda executa (Retry-After: 1) |
Espere e repita. |
| 409 | duplicate_external_id |
não | external_id já usado com outra chave |
Leia existing e concilie. |
| 409 | invalid_state_transition |
não | Ação que o estado atual não permite (cancelar saque já enviado, cancelar depósito pago, recall de saque PIX) |
Consulte o estado atual. |
| 409 | day_open |
sim | GET /reconciliation de um dia que ainda não fechou; detail traz o instante de abertura; Retry-After em segundos |
Agende após o instante; não repita em laço. |
| 412 | etag_mismatch |
não | If-Match desatualizado em POST /rules/change-requests |
Releia GET /rules e reenvie. |
| 413 | validation_failed |
não | Corpo acima de 64 KiB (errors[0] = {field: "body", reason: "too_large"}) |
Reduza o corpo. |
| 422 | amount_below_minimum, amount_above_maximum |
não | Limite por transação | Mostre o limite ao jogador. |
| 422 | document_invalid |
não | Documento do jogador inválido (CPF/CNPJ, dígitos verificadores) | Revise o cadastro do jogador. |
| 422 | document_mismatch |
não | Documento diferente do já fixado para esse external_ref |
Revise o cadastro do jogador. |
| 422 | player_blocked, destination_blocked |
não | Listas de bloqueio (mensagem genérica de propósito) | Não revele o motivo ao jogador. |
| 422 | velocity_exceeded |
não | Limite de frequência ou volume | Mostre "tente mais tarde". |
| 422 | insufficient_operation_balance |
não | Só saque: sem caixa livre na operação | Alerte o financeiro; nova tentativa é operação nova (external_id novo). |
| 422 | fx_not_enabled |
não | POST /fx/quotes: câmbio desligado em toda operação |
Não use câmbio. |
| 429 | rate_limited |
sim | Limite de taxa (Retry-After) |
Limites e quotas. |
| 500 | internal_error |
sim | Erro interno (detail: "erro interno; informe o trace_id") |
Repita com a mesma chave; se persistir, abra chamado com o trace_id. |
| 503 | service_unavailable |
sim | Dependência do Dracma fora, ou queda simulada em operação Teste | Espere o Retry-After e repita. |
Os motivos exatos dos 422 de dinheiro (limites, listas, frequência) dependem das regras da operação: consulte
GET /v1/rules e Decisão, limites e regras.
Reservado no contrato
Sete códigos do contrato 1.1.0 existem no catálogo mas a API não os emite hoje: psp_unavailable,
outside_operating_hours, fx_quote_expired, fx_quote_mismatch, corridor_not_allowed,
beneficiary_bank_unreachable e recall_window_expired. Quando um PSP está fora você recebe
service_unavailable. Trate-os como qualquer code desconhecido: pela classe HTTP e pelo retryable.
Motivos de validação: errors[].reason¶
Em 400 validation_failed, cada item de errors[] traz o field (por exemplo player.document.number; para
cabeçalhos, o nome do cabeçalho; para o corpo inteiro, body) e um reason. A lista é aberta: trate um reason
desconhecido como erro de validação genérico. São 22 motivos emitidos hoje:
reason |
Quando |
|---|---|
unknown_field |
Campo fora do schema (inclusive status, version, paid_amount, decision, operation_id aninhado) |
duplicate_key |
Chave JSON repetida em qualquer nível, inclusive variando a caixa |
invalid_utf8 |
Corpo com UTF-8 inválido |
syntax |
JSON malformado |
trailing_data |
Conteúdo depois do objeto JSON |
type |
Tipo errado (número onde vai string, por exemplo) |
invalid |
Corpo inválido por outro motivo (por exemplo amount fora do formato de Money) |
invalid_json |
Corpo de POST /fx/quotes que não é JSON válido |
required |
Campo obrigatório ausente; também Idempotency-Key ausente ou fora do formato |
format |
Formato inválido: external_id, external_ref, documento, país, IP, e-mail, updated_since, cursor, after |
enum |
Valor fora do enum: method, player.document.type, destination.pix.key_type, status de saque |
range |
Faixa: expires_in_seconds, metadata com mais de 20 chaves, limit fora de 1 a 200, name/device_id longos no saque |
one_of |
destination com zero ou mais de um método |
method_mismatch |
O método do destination difere de method |
currency_mismatch |
paid_amount.currency diferente da moeda do depósito em /sandbox/deposits/{id}/pay |
unknown |
after de GET /events que não é um evento da sua operação |
too_large |
Corpo acima de 64 KiB (status 413) |
required_by_operation |
Campo que a operação exige e o contrato deixa opcional (por exemplo player.email) |
max_length |
player.email com mais de 254 caracteres, player.name com mais de 120 no depósito; justification do recall com mais de 1000 |
reserved_domain_required |
Em operação Teste, player.email que não é de domínio reservado (.test, .invalid, .example) |
rail_not_enabled |
Também aparece como motivo em POST /sandbox/bank-transfers/incoming |
length |
justification de POST /rules/change-requests fora de 1 a 1000 caracteres |
status com valor desconhecido e limit fora da faixa aparecem como format em GET /deposits e como enum e
range em GET /withdrawals; não dependa da diferença. A resposta de validação de GET /events, /reconciliation,
/balances, /rules e /players/{ref}/limits traz message fixa ("parâmetro inválido").
Exemplo de campo desconhecido no corpo (o servidor recusa additionalProperties):
{
"type": "https://docs.dracma.test/errors/validation_failed",
"title": "validation_failed",
"status": 400,
"code": "validation_failed",
"trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
"retryable": false,
"errors": [
{ "field": "paid_amount", "message": "campo não permitido", "reason": "unknown_field" }
]
}
Respostas da borda (sem problem+json)¶
Antes de a requisição chegar à API, a entrada pública do Dracma pode responder sozinha. Essas respostas não têm
corpo problem+json:
| Resposta | Quando |
|---|---|
404 sem corpo |
Qualquer caminho fora de /v1/* (/health, /metrics, /); /v1/sandbox/* em operação Real (nunca existe) |
413 sem corpo |
Corpo acima de 1 MB (a API só chega a avaliar até 64 KiB) |
Falhas de TLS ou de rede também não têm corpo. O conector deve tratar uma resposta que não é JSON como falha de
transporte: repita com a mesma Idempotency-Key.
Rotas, métodos e erros genéricos¶
- Rota inexistente em
/v1com credencial válida:404 not_found. A autenticação vem antes: com assinatura inválida a resposta é401. - Método errado. Um
GETem uma rota que só aceitaPOSTvolta404 not_found. Qualquer método além deGET,HEADePOST, ou umPOSTem rota que não existe, volta403 insufficient_scope, porque o escopo é conferido antes da rota. - Erro genérico do servidor HTTP (caminho malformado, cabeçalho inválido, 4xx que não é do Dracma) vira
400 validation_failed; o corpo não trazerrors[]. - Erro inesperado vira
500 internal_error, sempre opaco. O motivo real fica no log do Dracma, ligado aotrace_id.
Modo degradado¶
O Dracma não "aceita sem checar": se uma dependência está fora, a rota recusa ou desvia para revisão. A tabela mostra o que você vê.
| Dependência fora | POST /deposits |
POST /withdrawals |
Leituras |
|---|---|---|---|
| Motor de regras | 503 service_unavailable, nenhum depósito criado |
201 com o saque em under_review e RULES_UNAVAILABLE em decision.reasons (revisão manual) |
GET /rules e GET /players/{ref}/limits: 503 |
| Livro de saldos | segue | segue; o motor pode registrar BALANCE_UNAVAILABLE e mandar para revisão, nunca liberar |
GET /balances e GET /reconciliation: 503 |
| Serviço de webhooks | segue; os eventos esperam e são entregues depois | segue | GET /reconciliation, GET /sandbox/webhooks e redeliver: 503; GET /events continua |
| Executor de pagamentos | 202 com o depósito em created até a cobrança sair; acompanhe por webhook ou GET /deposits/{id} |
201; o envio ao banco é retomado depois |
ações de sandbox: 503 com Retry-After: 5 |
| Armazenamento de nonces, cofre de segredos, limitador, idempotência | 503 com Retry-After: 1, antes de qualquer efeito |
idem | idem |
Em todos os 503 a regra é a mesma: nenhum efeito parcial; espere o Retry-After e repita com a mesma
Idempotency-Key. Um saque em under_review por RULES_UNAVAILABLE não é erro: veja
Decisão, limites e regras.
Exemplos¶
Recusa por limite de taxa. Repare em retryable: true e no Retry-After:
HTTP/1.1 429 Too Many Requests
Content-Type: application/problem+json
Retry-After: 2
Dracma-Trace-Id: 4bf92f3577b34da6a3ce929d0e0e4736
{
"type": "https://docs.dracma.test/errors/rate_limited",
"title": "rate_limited",
"status": 429,
"code": "rate_limited",
"trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
"retryable": true
}
Um tratador mínimo, em pseudocódigo:
resposta = enviar(pedido)
se resposta.status é 2xx: gravar e seguir
se resposta.json.retryable ou status é 5xx ou 429 ou erro de rede:
esperar Retry-After (ou backoff com jitter) e repetir com a MESMA Idempotency-Key
senão: registrar code, errors[] e trace_id; corrigir ou encerrar o fluxo
Erros e casos de borda¶
- Recusa de negócio não cria objeto. Um
4xxemPOST /depositsouPOST /withdrawalsnão cria depósito nem saque. No saque, devolva a reserva ao jogador ao receber um4xxconclusivo, e só então. A mesma chave devolve a mesma recusa por 30 dias; uma nova tentativa é uma operação nova, comexternal_ide chave novos. - Erro de validação também fica gravado. Um
400por corpo inválido é guardado com a chave; corrigir o corpo exige uma chave nova (mesmo corpo com a chave antiga devolve o mesmo400). 409 duplicate_external_idtraz o objeto existente emexisting: use-o para conciliar em vez de criar outro.- Motivos de revisão não são erros. Quando um saque é aceito mas vai para
under_review, os motivos vêm emdecision.reasons, não emerrors[].