Cobrança em cripto e links de pagamento

Para: Dev — Conferido em 2026-11-14.

Esta página descreve como cobrar em BRL e receber em cripto pela API (method: "crypto" em POST /v1/deposits), como mostrar ao jogador quanto ele paga antes de cobrar (GET /v1/fx/quote) e como criar um link de pagamento público (/v1/payment-links). Autenticação, Idempotency-Key, erros problem+json, paginação e webhooks são os mesmos do restante da API; aqui está só o que muda.

O que é

A cobrança continua em BRL; o jogador a quita no ativo escolhido (BTC.BTC, USDT.TRON e os demais de Cripto). A operação recebe o ativo, não BRL. O Dracma trava a cotação na criação, reserva um endereço único para a cobrança e devolve tudo o que o jogador precisa para pagar.

Rota Escopo operationId Resposta
GET /v1/fx/quote fx:read cotarCripto 200 com a cotação indicativa (binding: false)
POST /v1/deposits com method: "crypto" deposits:write criarDeposito 201 com instructions.crypto, ou 202 em created sem instruções
GET /v1/deposits/{id} read obterDeposito 200 com o Deposito, incluindo crypto (progresso)
POST /v1/payment-links payment_links:write criarLinkDePagamento 201 com o LinkDePagamento
GET /v1/payment-links payment_links:read listarLinksDePagamento 200 com {data, next_cursor}
GET /v1/payment-links/{id} payment_links:read obterLinkDePagamento 200 com o LinkDePagamento
POST /v1/payment-links/{id}/cancel payment_links:write cancelarLinkDePagamento 200, idempotente

Cripto vem desligada e é habilitada por operação. Com ela desligada, ou com o ativo fora da lista da operação, as rotas respondem 403 crypto_not_enabled. GET /v1/currencies mostra o que a sua operação aceita.

Saque em cripto

POST /v1/withdrawals não aceita cripto nesta versão. Esta página trata só de cobrança (depósito) e de links.

Quando usar

  • Cobrança direta (POST /v1/deposits): o seu produto já sabe o valor e o ativo e mostra o QR ao jogador, ou o leva ao checkout_url.
  • Cotação antes de cobrar (GET /v1/fx/quote): para exibir "R$ 100,00 = 0,00021053 BTC" na tela de escolha. Não reserva nada.
  • Link de pagamento: você precisa de uma URL para mandar por mensagem ou e-mail e deixar o pagador escolher o método. O link não é uma cobrança: a cobrança nasce quando o pagador age na página pública.

Como funciona

Cotar: GET /v1/fx/quote

Parâmetro Obrigatório Regra
amount sim BRL na menor unidade (centavos), inteiro em texto, por exemplo 10000 para R$ 100,00
currency não só BRL (padrão)
pay_currency sim ativo, por exemplo BTC.BTC

A resposta (CotacaoCripto) traz amount, pay_currency, pay_amount (unidade mínima do ativo), pay_amount_decimal, rate, mid_rate, spread_bps, quoted_at, expires_at e binding: false. Pontos que costumam enganar:

  • binding: false quer dizer que nada foi travado. O valor travado só nasce no POST /v1/deposits (ou na ação do pagador num link) e pode ser diferente do que a cotação mostrou.
  • rate é BRL por 1 unidade maior do ativo (1 BTC), já com o spread. Não é a taxa inversa do câmbio de carteiras (POST /v1/fx/quotes, outra rota, outra semântica).
  • spread_bps é o da configuração da operação. Mandar spread_bps na query é ignorado.
  • Dinheiro é sempre texto, nunca número JSON. pay_amount_decimal é decimal exato, sem arredondamento de ponto flutuante.
  • A rota tem limite próprio de 60 por minuto por operação e responde Cache-Control: no-store.

Criar a cobrança: POST /v1/deposits com method: "crypto"

O corpo é o mesmo de Depósitos, com estas regras a mais:

Campo Regra
amount em BRL. Outra moeda responde 400 validation_failed com errors[0].reason = crypto_amount_must_be_brl
method crypto
method_options.crypto.pay_currency obrigatório: o ativo, dentre os habilitados da operação
spread_bps, rate, pay_amount campos desconhecidos: 400. A cotação é do Dracma, nunca do cliente
expires_in_seconds ignorado: a validade é a da cotação
fx_quote_id 422 fx_quote_not_applicable
POST /v1/deposits HTTP/1.1
Host: api.example.test
Content-Type: application/json; charset=utf-8
Idempotency-Key: dep_0192a3b4-5c6d-7e8f-9a0b-1c2d3e4f5a6b
Dracma-Key-Id: key_test_01J9EXEMPLO
Dracma-Timestamp: 1790000000
Dracma-Nonce: 0123456789abcdef0123456789abcdef
Dracma-Signature: v1=<assinatura>

{
  "external_id": "dep_0192a3b4-5c6d-7e8f-9a0b-1c2d3e4f5a6b",
  "player": {
    "external_ref": "0192a3b4-0000-7000-8000-000000000001",
    "document": { "type": "CPF", "number": "12345678909", "country": "BR" }
  },
  "amount": { "amount": "10000", "currency": "BRL" },
  "method": "crypto",
  "method_options": { "crypto": { "pay_currency": "BTC.BTC" } }
}

O 201 traz status: "pending" e instructions.crypto (InstrucoesCripto):

{
  "id": "dep_0192a3b40c6d7e8f9a0b1c2d3e4f5a6b",
  "external_id": "dep_0192a3b4-5c6d-7e8f-9a0b-1c2d3e4f5a6b",
  "status": "pending",
  "version": 2,
  "amount": { "amount": "10000", "currency": "BRL" },
  "method": "crypto",
  "player_ref": "0192a3b4-0000-7000-8000-000000000001",
  "provider": "chain",
  "instructions": {
    "crypto": {
      "address": "tb1qexemplo0000000000000000000000000000",
      "network": "bitcoin",
      "pay_currency": "BTC.BTC",
      "pay_amount": { "amount": "21053", "currency": "BTC.BTC" },
      "pay_amount_decimal": "0.00021053",
      "rate": "475000.00",
      "mid_rate": "500000.00",
      "spread_bps": 500,
      "quoted_at": "2026-11-14T12:00:00Z",
      "expires_at": "2026-11-14T12:15:00Z",
      "payment_uri": "bitcoin:tb1qexemplo0000000000000000000000000000?amount=0.00021053",
      "checkout_url": "https://pay.example.test/c/chk_exemploexemploexemploexemploexemploexe",
      "confirmations_required": 3,
      "testnet": true
    }
  },
  "crypto": null,
  "created_at": "2026-11-14T12:00:00Z",
  "updated_at": "2026-11-14T12:00:01Z",
  "expires_at": "2026-11-14T12:15:00Z"
}
Campo de instructions.crypto Uso
address, network endereço único desta cobrança e a rede. Nunca reutilizado, sem memo nem tag
pay_amount valor a enviar, em unidade mínima do ativo (satoshis, no exemplo), texto inteiro
pay_amount_decimal o mesmo valor em decimal exato; é o que vai no QR
rate, mid_rate, spread_bps cotação travada: com spread, sem spread e o spread aplicado
quoted_at, expires_at validade da cotação; expires_at é também o expires_at do depósito
payment_uri URI para o QR (BIP-21, EIP-681 ou URI TRON). Nem toda carteira lê a URI: mostre também endereço e valor em texto
checkout_url página hospedada de pagamento. É uma URL-capacidade: quem a tem consegue vê-la. Trate como segredo curto
confirmations_required confirmações da rede antes de paid
testnet true fora de produção: o ativo é de rede de testes e não vale nada
  • Replay: a mesma Idempotency-Key devolve as mesmas instruções (mesmo endereço, mesma cotação, mesmo expires_at), nunca uma cotação nova.
  • 202: a cadeia ainda está reservando o endereço. status vem created e instructions vem null. Espere deposit.pending ou consulte o depósito; não crie outro.
  • 503 quote_unavailable: a cotação ou a rede está fora (Retry-After, reason em sources_diverge, stale, insufficient_sources, upstream_down, invalid_rate ou store_unavailable). Nada foi criado. Repita com a mesma Idempotency-Key.

Estados da cobrança em cripto

stateDiagram-v2 [*] --> created created --> pending pending --> detected: tx vista na rede detected --> confirming: 1 confirmação ou mais confirming --> paid: confirmações necessárias pending --> expired: prazo sem pagamento pending --> cancelled expired --> under_review: pagamento tardio pending --> under_review: valor ou ativo errado detected --> under_review confirming --> under_review detected --> pending: reorg antes de confirmar paid --> chargeback: reorg depois de pago
status Credita? Significado
pending não instruções emitidas, aguardando o pagamento
detected não a rede viu uma transação para o endereço (0 confirmações)
confirming não já tem confirmação, mas menos que confirmations_required; crypto.confirmations mostra {current, required}
paid sim a soma dos recibos confirmados é igual a pay_amount e o primeiro foi visto até expires_at
expired não o prazo passou sem nenhum recibo visto
under_review não dinheiro em custódia à parte, esperando decisão manual (abaixo)

Só paid credita. detected e confirming nunca movem dinheiro, nem no seu livro. Vale o instante em que o recibo foi visto, não o da confirmação: pagar dentro do prazo e confirmar depois do expires_at ainda resulta em paid.

under_review vem com crypto.review_reason e o equivalente em maiúsculas em review_reasons:

crypto.review_reason review_reasons Quando
late_payment PAID_AFTER_EXPIRY recibo visto depois do expires_at
amount_mismatch AMOUNT_MISMATCH soma confirmada diferente de pay_amount, para menos ou para mais. Não existe paid_partial em cripto
wrong_asset WRONG_ASSET chegou ao endereço outro ativo suportado, por exemplo USDC.ETH numa cobrança ETH.ETH

A revisão é resolvida no painel pelo Dracma. Não credite o jogador enquanto o estado for under_review.

Reorganização da rede. Antes de paid, a cobrança volta a pending sem crédito. Depois de paid, o Dracma emite deposit.chargeback: desfaça o crédito do seu lado quando o fetch-back mostrar chargeback.

O objeto crypto (progresso)

Preenchido só em method: "crypto", e null até haver recibo.

Campo Conteúdo
txid identificador da transação, ou null
confirmations {current, required}
received soma on-chain recebida, em Money do ativo
credited creditado à operação, no ativo. Só em paid
spread_amount received menos credited. Só em paid
detected_at, confirmed_at RFC 3339, ou null
review_reason motivo de under_review, ou null

O seu livro de jogador continua em BRL: ao ver paid no fetch-back, credite o jogador por amount (BRL), como em PIX. credited e spread_amount descrevem o que o Dracma credita à operação em ativo; não os use para creditar o jogador.

Webhooks

Assinados e entregues como os demais (veja Webhooks): at-least-once, com fetch-back obrigatório.

Evento Quando data.object
deposit.crypto.detected detected Deposito
deposit.crypto.confirmations crypto.confirmations.current mudou (no máximo um por 30 s por cobrança; o último antes de paid sempre sai) Deposito
deposit.paid, deposit.under_review, deposit.expired, deposit.cancelled, deposit.chargeback os existentes Deposito
payment_link.paid a cobrança do link ficou paid LinkDePagamento

O receptor deve tratar tipo desconhecido com 200 e sem mover dinheiro. Para payment_link.paid, o last_charge_id aponta a cobrança: confira GET /v1/payment-links/{id} e GET /v1/deposits/{id} antes de creditar.

Um link é uma URL pública, https://pay.<domínio>/l/<slug>, que deixa o pagador escolher o método. O url devolvido pela API já vem completo. O slug (128 bits) é a capacidade pública e é diferente do id (plk_...); usar o slug no lugar do id nas rotas da API responde 404.

sequenceDiagram participant C as Seu conector participant D as API do Dracma participant P as Pagador participant K as Página pay C->>D: POST /v1/payment-links (Idempotency-Key) D-->>C: 201 url + id plk_ C->>P: envia a url P->>K: abre /l/slug e escolhe PIX ou ativo K->>D: cria a cobrança (servidor a servidor) K-->>P: checkout com QR, valor e estado D-->>C: webhook payment_link.paid C->>D: GET /v1/payment-links/{id} e GET /v1/deposits/{id}

Corpo de POST /v1/payment-links (CriarLink; campo desconhecido é 400):

Campo Obrigatório Regra
external_id sim seu id, até 64 caracteres, único por operação. Repetido com outra chave de idempotência: 409 duplicate_external_id
player sim o jogador, com documento, como em depósito. Fica fixado no link: todas as cobranças dele são atribuídas a esse jogador
amount_type sim fixed ou open
amount se fixed Money em BRL. Proibido em open
min_amount, max_amount se open Money em BRL, ambos obrigatórios, max_amount até o teto da operação. Proibidos em fixed
allowed_methods sim de 1 a 2 itens: {"method": "pix"} e/ou {"method": "crypto", "assets": ["BTC.BTC"]}. Em crypto, assets é obrigatório, sem repetição, e dentro dos ativos habilitados
usage não single (padrão: uma cobrança paga encerra o link) ou multi
max_uses só multi de 2 a 100000 cobranças pagas que encerram o link. Ausente: ilimitado até expirar
description não até 140 caracteres, texto puro, sem HTML nem dado pessoal. É mostrada ao pagador
expires_at não padrão agora mais 7 dias; mínimo 5 minutos, máximo 90 dias
metadata não até 20 chaves, valores texto de até 256 caracteres
{
  "external_id": "link-2026-11-0001",
  "player": {
    "external_ref": "0192a3b4-0000-7000-8000-000000000001",
    "document": { "type": "CPF", "number": "12345678909", "country": "BR" }
  },
  "amount_type": "fixed",
  "amount": { "amount": "10000", "currency": "BRL" },
  "allowed_methods": [
    { "method": "pix" },
    { "method": "crypto", "assets": ["BTC.BTC", "USDT.TRON"] }
  ],
  "usage": "single",
  "description": "Recarga de saldo",
  "expires_at": "2026-11-21T12:00:00Z"
}

Resposta 201 (LinkDePagamento):

{
  "id": "plk_01J9ZQ3EXEMPLO",
  "external_id": "link-2026-11-0001",
  "status": "active",
  "version": 1,
  "usage": "single",
  "amount_type": "fixed",
  "amount": { "amount": "10000", "currency": "BRL" },
  "min_amount": null,
  "max_amount": null,
  "allowed_methods": [
    { "method": "pix" },
    { "method": "crypto", "assets": ["BTC.BTC", "USDT.TRON"] }
  ],
  "player_ref": "0192a3b4-0000-7000-8000-000000000001",
  "description": "Recarga de saldo",
  "url": "https://pay.example.test/l/Abcdefghijklmnopqrstuv",
  "max_uses": null,
  "uses_paid": 0,
  "last_charge_id": null,
  "expires_at": "2026-11-21T12:00:00Z",
  "paid_at": null,
  "cancelled_at": null,
  "metadata": {},
  "created_at": "2026-11-14T12:00:00Z",
  "updated_at": "2026-11-14T12:00:00Z"
}
status do link Significado
active pode receber cobranças
paid encerrado por pagamento (single, ou multi que atingiu max_uses)
expired passou do expires_at
cancelled cancelado por você
  • Em single existe no máximo uma cobrança não terminal por vez.
  • Cancelar (POST /v1/payment-links/{id}/cancel, Idempotency-Key obrigatória): active passa a cancelled. Cobranças do link ainda pending, sem pagamento detectado, são canceladas junto. Uma cobrança com pagamento detected ou confirming segue até o fim, porque o dinheiro já está a caminho; se ficar paid num link single, o link vira paid. Link paid ou expired responde 409 invalid_state_transition. Repetir o cancelamento de um link cancelled devolve 200 com o mesmo corpo.
  • Listar (GET /v1/payment-links): filtros external_id, status e player_ref, mais cursor e limit (1 a 200; fora disso 400). Só aparecem links da sua operação.

O que o pagador vê na página pública

A página pay é só front: o servidor dela fala com a API por assinatura entre serviços, e nenhum segredo vai ao navegador. Ela mostra o valor em BRL, o ativo, a rede, o endereço, o valor exato a enviar, o QR, a cotação, a contagem regressiva e o estado, e se atualiza sozinha. Ela não mostra nem devolve: identificadores internos (dep_, plk_), o seu external_id, o jogador e o documento, metadata, o spread, a cotação sem spread, tarifas nem o nome do provedor.

  • Um token ou slug desconhecido, malformado ou de outro ambiente responde 404 idêntico: não há como descobrir quais existem.
  • Link paid, expired ou cancelled aberto na rota pública responde 410 payment_link_unavailable.
  • O estado visível ao pagador segue os da tabela acima, em texto simples (por exemplo "Aguardando pagamento", "Confirmando", "Pagamento confirmado", "Pagamento em análise"). O pagamento só vale quando o seu fetch-back lê paid.
  • Fora de produção o checkout mostra um aviso de rede de testes.

Erros

code HTTP Repetir? Quando
crypto_not_enabled 403 não cripto desligada na operação, ou ativo fora da lista habilitada
asset_not_supported 422 não ativo desconhecido ou fora das capacidades do provedor
quote_unavailable 503 sim, com Retry-After fontes divergem, velhas ou insuficientes; cotação ou rede fora. O reason diz qual
fx_quote_not_applicable 422 não fx_quote_id num depósito crypto
amount_out_of_range 422 não valor fora da faixa mínima e máxima da operação (ou do link aberto)
payment_link_unavailable 410 não link pago, expirado ou cancelado, na rota pública
payment_link_method_not_allowed 422 não método ou ativo fora de allowed_methods
invalid_state_transition 409 não por exemplo cancelar um link já pago
duplicate_external_id, idempotency_key_reused 409 não os existentes

Em POST /v1/deposits com cripto, a ordem de avaliação é: autenticação e escopo, validação do corpo, crypto_not_enabled, asset_not_supported, amount_out_of_range, regras, cotação (quote_unavailable) e endereço. O primeiro que falha decide a resposta. Os códigos estão no formato problem+json de Erros.

Erros comuns

Sintoma Causa provável Correção
403 crypto_not_enabled em operação nova cripto vem desligada peça a habilitação ao time do Dracma e confira GET /v1/currencies
400 ao mandar spread_bps ou rate a cotação é do Dracma remova os campos
Creditou o jogador em detected tratou "visto na rede" como pago credite só com paid lido no fetch-back
Valor do QR diferente do que a cotação mostrou GET /v1/fx/quote é indicativa mostre o valor de instructions.crypto, que é o travado
404 ao consultar o link pelo url usou o slug no lugar do id consulte por plk_...
Pagou na rede errada rede ou ativo diferente do indicado o pagamento cai em wrong_asset ou pode não ser recuperável; avise o jogador na sua tela

Veja também