Depósito no nero_payments

Para: Dev — Conferido em 2026-10-18.

Como implementar POST /api/payments/deposits e GET /api/payments/deposits/{deposit_id} sobre o POST /v1/deposits do Dracma. A referência da API está em Depósitos.

O que é

A rota do portal recebe o pedido do jogador em reais, converte para a menor unidade, grava o intent e chama o Dracma. Na Nero há três particularidades: a conversão do valor vindo do front, as exigências por operação (que mudam com o PSP) e o campo accept_bonus, que fica de fora da chamada.

Quando usar

Ao implementar ou revisar a rota de depósito do nero_payments.

Como funciona

Passo a passo do POST /api/payments/deposits

flowchart TD A["Pedido do front com X-Nero-Identity"] --> B{"Corpo válido? amount em reais com até 2 casas, method = pix"} B -->|não| B1["400 ao front"] B -->|sim| C["Carrega o jogador pelo id da asserção: external_ref, CPF, nome, e-mail, IP"] C --> D{"Exigências da operação (GET /v1/currencies em cache) atendidas?"} D -->|não| D1["422 ao front: complete o cadastro, sem chamar o Dracma"] D -->|sim| E["Transação: grava intent dep_uuidv7 com os bytes do corpo"] E --> F["POST /v1/deposits com Idempotency-Key igual ao external_id, timeout 10 s"] F -->|"201 pending"| G["Aplica o estado e devolve 201 com o pix"] F -->|"202 created"| H["Devolve 202 com status creating"] F -->|"4xx"| I["Intent rejected e erro mapeado"] F -->|"timeout, 5xx, 429"| J["Intent segue pending no outbox e 202 creating"]
  1. Identidade. O jogador vem da asserção X-Nero-Identity do portal. Nada de player_id, e-mail ou CPF de outro jogador vindo do corpo.
  2. Valor sem float. O front manda amount em reais (50.00). Converta para a menor unidade por texto, nunca por multiplicação de ponto flutuante (Convenções HTTP):
Entrada (amount) Menor unidade (amount.amount)
50 "5000"
50.5 "5050"
50.00 "5000"
0.01 "1"
50.001, -1, 0, 1e3, "50,00" Recuse com 400
centavos(numero_json_como_texto):
  exigir casa com ^[0-9]{1,12}(\.[0-9]{1,2})?$      # sem sinal, sem expoente, até 2 casas
  inteiro, frac = separar no "."; frac = (frac + "00")[0:2]
  c = remover_zeros_a_esquerda(inteiro + frac); exigir c != "0"
  devolver c                                         # string de inteiro

Em Go, leia o corpo com json.Decoder.UseNumber(). Em Node, não use JSON.parse direto no valor: leia o número como texto ou use um parser que preserve o literal. 3. Exigências da operação. Antes de montar o corpo, confira deposit_requirements do GET /v1/currencies (em cache; recarregue no boot e a cada hora). Com o Mercado Pago como PSP de depósito, o Dracma exige player.name e player.email; com a MaximaxPay, player.name. Se o jogador não tem o campo, responda ao front pedindo para completar o cadastro, sem chamar o Dracma. 4. Documento. O cpf do corpo só é aceito se for o CPF do cadastro do jogador (ou o primeiro cadastro dele). O Dracma fixa o documento no primeiro uso: documento diferente depois dá 422 document_mismatch. 5. Outbox. Na mesma transação, gere external_id = dep_<uuidv7>, serialize o corpo uma vez e grave os bytes. A Idempotency-Key é o próprio external_id. 6. Chamada. POST /v1/deposits assinado, timeout de 10 segundos. Trate a resposta pela tabela abaixo.

Corpo enviado ao Dracma

{
  "external_id": "dep_0192a3b4-5c6d-7e8f-9a0b-1c2d3e4f5a6b",
  "player": {
    "external_ref": "0192a3b4-0000-7000-8000-000000000001",
    "document": { "type": "CPF", "number": "52998224725", "country": "BR" },
    "name": "Jogador Teste",
    "email": "jogador1@nero.test",
    "ip": "203.0.113.10"
  },
  "amount": { "amount": "5000", "currency": "BRL" },
  "method": "pix",
  "expires_in_seconds": 1800
}
Campo Origem na Nero Nota
external_id Gerado pelo nero_payments (dep_<uuidv7>) É o deposit_id do front e a Idempotency-Key
player.external_ref Id interno imutável do jogador (IAM da Nero) Nunca e-mail, telefone ou CPF
player.document CPF do cadastro, só dígitos Só CPF ou CNPJ em PIX
player.name Nome do cadastro Exigido quando a operação pede (até 120 caracteres)
player.email E-mail cadastrado do jogador Só quando a operação exige (ou para risco). No hml, só .test, .invalid ou .example
player.ip IP do jogador que o portal recebeu do Caddy Ajuda o risco; pode ser exigido por operação
amount Menor unidade, da conversão acima String de inteiro
expires_in_seconds Opcional (300 a 86400; padrão 1800) O prazo efetivo vem em instructions.pix.expires_at
accept_bonus Não vai ao Dracma Bônus é regra da bet (Arquitetura do conector)

Nunca envie operation_id, status, version, paid_amount, method_options ou fx_quote_id: o Dracma recusa campo desconhecido com 400 (exceto operation_id na raiz, que ele descarta).

Resposta do Dracma e resposta ao front

Dracma Outbox Front
201, status: pending acknowledged; grava dracma_id, version, instructions 201 {deposit_id, status: "pending", amount, currency: "BRL", method: "pix", pix: {copy_paste, qr_code_png_base64?, expires_at}, expires_at}; use o expires_at do Dracma
201, status: failed acknowledged (terminal) 201 {status: "failed"}
202, status: created acknowledged 202 {deposit_id, status: "creating"}; o PIX chega por deposit.pending
400 validation_failed com required_by_operation, reserved_domain_required ou format rejected 422 "complete seu cadastro" (e-mail)
Outro 400 validation_failed rejected e alerta (bug do conector) 500 genérico ao front
400 currency_not_enabled ou method_not_enabled rejected e alerta 503 "depósito indisponível"
409 duplicate_external_id acknowledged com o objeto de existing Como 201 ou 202, conforme o estado de existing
409 idempotency_key_reused needs_decision e alerta (bug) 500 genérico
422 document_invalid ou document_mismatch rejected 422 "confira seu CPF"
422 amount_below_minimum ou amount_above_maximum rejected 422 com o limite (leia de GET /v1/rules)
422 player_blocked ou velocity_exceeded rejected 422 "não foi possível gerar o depósito agora" (genérico, sem motivo)
429 rate_limited (limite de depósitos por jogador por hora) pending (repete depois do Retry-After), ou rejected se a Nero preferir desistir 429 "muitas tentativas, aguarde"
503, 5xx ou timeout pending, backoff, mesma chave 202 {status: "creating"}

4xx não cria objeto no Dracma e não se repete. Tentar de novo depois de corrigir o cadastro usa um novo external_id, portanto um novo deposit_id.

GET /api/payments/deposits/{deposit_id}

  • Só o dono: deposit_id de outro jogador devolve 404, a mesma resposta de "não existe".
  • Lê o estado local e traduz pela tabela de Estados no front. Não é proxy do Dracma.
  • Opcional: se o intent está parado em creating ou pending há mais de 60 segundos, faça um GET /v1/deposits/{dracma_id} (no máximo a cada 10 segundos por depósito). Sem dracma_id (resposta perdida), use GET /v1/deposits?external_id=, que está no balde de listagem (5 req/s por chave): nunca por polling do front.

Crédito

O crédito acontece só no caminho do webhook ou do job de GET /events (Webhook, eventos e reconciliação), depois do fetch-back, e é idempotente pelo external_id:

-- no nero_ledger, na mesma transação que marca o evento como visto e o intent como paid
INSERT INTO lancamento (origem, external_id, jogador_id, valor_minor, tipo)
VALUES ('dracma', $1, $2, $3, 'credito_deposito')
ON CONFLICT (origem, external_id, tipo) DO NOTHING;   -- restrição única: 1 crédito por depósito

O valor creditado é o amount_minor do intent (o que a Nero pediu), depois de conferir que o amount.amount do fetch-back é idêntico. Divergência: não credita, abre needs_decision e dispara alerta (nero_dracma_fetchback_mismatch_total).

Exemplos

Os comandos para criar um depósito de teste e vê-lo pago estão em Primeiros passos no sandbox.

Erros e casos de borda

  • Jogador sem e-mail cadastrado numa operação que exige: devolva "complete seu cadastro" sem chamar o Dracma, para não gastar um external_id recusado.
  • Pagamento depois de expirar ou cancelar: o fetch-back mostra paid_amount num estado terminal. Não credite; abra needs_decision (D8).
  • Conferência por external_id depois de resposta perdida: GET /v1/deposits?external_id= devolve exatamente um depósito ou nenhum.

Veja também