Reconciliação¶
Para: Dev, Fin — Conferido em 2026-10-18.
GET /v1/reconciliation entrega, para cada dia UTC e cada moeda, a lista de movimentos liquidados, os totais e um digest assinado.
Com ele você prova todos os dias que o seu livro e o do Dracma contam o mesmo dinheiro. Esta página descreve os parâmetros, os itens,
os totais, o cálculo do digest e a verificação da assinatura.
O que é¶
| Rota | Escopo | operationId |
Resposta |
|---|---|---|---|
GET /v1/reconciliation |
read |
reconciliacao |
200 com uma página do relatório do dia, ou 409 day_open |
O relatório vem do fechamento do livro do Dracma, que é o dono do que está "liquidado". A API recalcula o digest por conta própria e o compara
com o do livro: se divergirem, ela responde 503 e alerta o Dracma, em vez de entregar um relatório inconsistente.
A reconciliação tem duas rotinas no seu lado:
- Diária: o relatório dos movimentos liquidados em cada dia UTC, por moeda, com digest assinado (esta página).
- A cada 5 minutos:
GET /v1/events?after=para recuperar webhooks perdidos (veja Recuperação comGET /events).
Quando usar¶
Uma vez por dia, depois das 02:00 UTC do dia seguinte, para cada moeda habilitada. O dia D só abre em D+1 às 02:00 UTC
(24 h mais 2 h de carência). Antes disso a resposta é 409 day_open.
| Quando | O quê |
|---|---|
| A cada 5 min | GET /v1/events?after=<último evt>: deduplique, faça o fetch-back e aplique o efeito |
| Diariamente, depois das 02:00 UTC | GET /v1/reconciliation do dia anterior, para cada moeda: digest, assinatura e comparação com o seu livro |
| Continuamente | GET /v1/balances para alertar caixa baixo (veja Limites do jogador e saldos) |
Para a visão do financeiro, no painel, veja Reconciliação no painel.
Como funciona¶
Parâmetros¶
| Parâmetro | Obrigatório | Conteúdo |
|---|---|---|
date |
sim | dia UTC, AAAA-MM-DD. Formato inválido (inclusive data que não existe): 400 com reason: format |
currency |
sim | moeda da carteira (BRL). Ausente: 400 com reason: required. Não habilitada: 400 currency_not_enabled |
cursor |
não | o next_cursor da página anterior. Inválido: 400 com reason: format |
limit |
não | de 1 a 200. Padrão 50. Fora da faixa: 400 com reason: range |
Disponibilidade e 409 day_open¶
Pedir um dia que ainda não abriu responde 409 com code: day_open:
retryable: true;Retry-Aftercom os segundos até a abertura (no mínimo 1);detailcom o instante de abertura:disponível a partir de <RFC 3339 UTC>.
O erro significa "tente mais tarde". Agende a chamada para depois das 02:00 UTC e não repita em laço.
Paginação, totais e assinatura¶
- A ordem é a canônica:
object_idcrescente e, em empate,kindcrescente. Ocursoré opaco. totalsedigestsó vêm na última página, a que traznext_cursor: null. Nas outras páginas os dois vêmnull.- O cabeçalho
Dracma-Signaturetambém só vem na última página. - A última página nunca sai sem assinatura. Se o assinador, o livro ou o banco estiverem fora, a resposta é
503 service_unavailable, repetível. - A rota usa o balde geral de leitura do rate limit. Pagine devagar mesmo assim. Veja Limites e quotas.
Os itens¶
Cada item de items é um movimento liquidado no dia:
| Campo | Tipo | Conteúdo |
|---|---|---|
object_id |
texto | o id do Dracma do depósito ou saque |
external_id |
texto | o seu id |
kind |
texto | o tipo do movimento (tabela abaixo) |
status |
texto | o status do objeto naquele momento: paid, refunded, chargeback ou returned |
amount |
Money |
o valor do movimento |
fee |
Money ou null |
sempre null hoje |
settled_at |
texto | RFC 3339 em UTC, com segundos e sem fração |
kind: o que o Dracma emite hoje
kind |
O que é | Emitido hoje? | amount |
settled_at |
|---|---|---|---|---|
deposit |
depósito liquidado (paid) |
sim | o valor pago | o paid_at do depósito |
withdrawal |
saque liquidado (paid) |
sim | o valor do saque | o paid_at do saque |
refund |
estorno de depósito | só no sandbox | o valor do depósito | o instante do evento |
chargeback |
chargeback ou disputa de depósito | só no sandbox | o valor do depósito | o instante do evento |
return |
dinheiro devolvido: depósito returned ou saque returned |
sim | depósito: o valor recebido (paid_amount). Saque: o return.returned_amount |
o instante do evento |
fee |
tarifa | não | — | — |
recall, receipt |
recall aceito e recebimento não casado, da v1.1.0 | não | — | — |
settled_atdedepositewithdrawalé opaid_atdo objeto. Nos demaiskindé o instante em que o Dracma registrou o evento. Compare pelosettled_atdo Dracma (UTC), não pela data local: perto da meia-noite UTC o mesmo movimento pode cair no dia vizinho do seu fuso.- Um depósito que foi para
returnedsem nunca ter sidopaidaparece só comreturn, sem itemdeposit. Isso é normal. - Um saque devolvido aparece com dois itens: o
withdrawaldo dia do pagamento e oreturndo dia da devolução. - Itens
feee o campofeedo item não existem hoje: o Dracma não desconta tarifa do crédito. As tarifas são por contrato (veja Tarifas). - Aplique o leitor tolerante:
kinddesconhecido entra no digest como qualquer outro, mas não gera lançamento automático no seu livro. Vira item de revisão.
Reservado no contrato
fee (item e total), kind: fee, recall e receipt estão no contrato 1.1.0 e no OpenAPI, mas o Dracma não emite esses itens hoje. O total fees vem "0".
Os totais¶
totals traz seis campos, calculados sobre todos os itens do dia (não só os da página):
| Campo | Soma |
|---|---|
deposits_paid |
itens deposit |
withdrawals_paid |
itens withdrawal |
refunds |
itens refund |
chargebacks |
itens chargeback |
fees |
itens fee e fee dos itens. Hoje sempre "0" |
count |
quantidade de itens, inclusive return |
Os totais não incluem return
A API não devolve um total de return. O item return conta em count, mas não entra em nenhuma soma. Portanto "os totais batem com a soma dos itens"
só vale quando o dia não tem return. Quando tiver, some os itens return à parte e compare com o seu livro. O documento OpenAPI servido também não
tem esse total.
O digest¶
Para cada item do dia, em todas as páginas, monte a linha canônica:
object_id|external_id|kind|status|amount|currency|settled_at
- Junte os itens de todas as páginas.
- Ordene por
object_idcrescente (comparação de bytes, sem locale). Em empate (o mesmo objeto em dois itens, por exemplowithdrawalereturn), ordene porkindcrescente. O contrato só citaobject_id; o desempate porkindé o que o Dracma faz (veja Divergências conhecidas). - Una as linhas com
\n. Pela definição de "unir", não há\nno final. digest = hex( sha256( linhas_unidas ) ), em minúsculas.- O resultado tem de ser igual ao
digestda última página.
Detalhes que costumam errar:
amounté o inteiro na menor unidade como vem no JSON ("5000", não50.00).currencyda linha é a moeda do item, igual aamount.currencye aocurrencydo topo.settled_até a string do JSON, sem reformatar.- Dia sem movimento:
items: []edigestigual ao sha256 de texto vazio,e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855.
A assinatura do digest¶
Dracma-Signature: t=<ts>,v1=hex( HMAC-SHA256( segredo_de_webhook, "<t>." + digest ) )
- O segredo é o segredo de webhook da operação (não o de chamada à API), com a mesma rotação do webhook. Podem vir dois
v1, e basta um bater. - A mensagem assinada é o
t, um ponto e odigestem hexadecimal (os 64 caracteres). Não é o corpo da resposta. - Verifique na hora do download. O Dracma assina a cada pedido da última página, então o
t=é sempre recente. Aplique a mesma janela de ±300 s do webhook sobre ot=. Guarde o cabeçalho junto com o relatório como prova; uma reverificação posterior compara só ov1, sem a janela. - A primitiva é a mesma do webhook (
"<t>." + mensagem): os clientes de Exemplos reaproveitam a verificação passando o digest no lugar do corpo. Veja Requisição e assinatura.
Fluxo completo¶
Pseudocódigo:
itens = []
cursor = nulo
repita:
r = GET /v1/reconciliation?date=D¤cy=BRL&limit=200[&cursor=cursor]
se r.status == 409 e r.code == "day_open": agendar para depois das 02:00 UTC; parar
itens += r.json.items
cursor = r.json.next_cursor
até cursor == nulo
ultima = r
linhas = [f"{i.object_id}|{i.external_id}|{i.kind}|{i.status}|{i.amount.amount}|{i.amount.currency}|{i.settled_at}"
para i em ordenar(itens, por=(object_id, kind))]
digest = sha256_hex(juntar(linhas, "\n"))
exigir digest == ultima.json.digest senão incidente
sig = parse(ultima.header["Dracma-Signature"]) # t= e 1..2 v1
exigir algum v1 == HMAC_SHA256(segredo_webhook_ativo, sig.t + "." + digest) # tempo constante
comparar_com_meu_livro(itens)
O que comparar com o seu livro¶
Para cada dia e cada moeda:
- Todo
depositdo Dracma tem exatamente um crédito no seu livro, com o mesmoexternal_ide o mesmo valor. - Todo crédito seu tem um
depositno relatório. Um crédito sem par é o caso mais grave: é dinheiro criado sem liquidação. - Todo
withdrawalcorresponde a uma reserva sua finalizada, e vice-versa. refund,chargebackereturncorrespondem às decisões que você registrou. Parareturn, veja Reconciliar com o seu livro.totalsbatem com a soma dos itens, exceto peloreturn(veja acima).
Divergência é incidente. Não "conserte" o livro para bater. Abra o incidente, congele o que for preciso, investigue pelo trace_id e pelos eventos
(GET /v1/events?object_id=) e registre. Dispare o alerta quando o digest não bate, quando a assinatura não bate ou quando falta um par.
Exemplos¶
GET /v1/reconciliation?date=2026-10-17¤cy=BRL&limit=200 HTTP/1.1
Host: api.example.test
Dracma-Key-Id: key_test_01J9EXEMPLO
Dracma-Timestamp: 1790000400
Dracma-Nonce: 0123456789abcdef0123456789abcdef
Dracma-Signature: v1=<assinatura>
Última página de um dia com um depósito, um saque e uma devolução parcial desse saque. Repare que count é 3 e que totals não tem soma de return:
HTTP/1.1 200 OK
Dracma-Signature: t=1790000401,v1=<64 caracteres hexadecimais>
{
"date": "2026-10-17",
"currency": "BRL",
"items": [
{
"object_id": "dep_0192a3b40c6d7e8f9a0b1c2d3e4f5a6b",
"external_id": "dep_0192a3b4-5c6d-7e8f-9a0b-1c2d3e4f5a6b",
"kind": "deposit",
"status": "paid",
"amount": { "amount": "5000", "currency": "BRL" },
"fee": null,
"settled_at": "2026-10-17T15:07:40Z"
},
{
"object_id": "wdr_0192a3b40c6d7e8f9a0b1c2d3e4f5a6c",
"external_id": "wdr_0192a3b4-5c6d-7e8f-9a0b-1c2d3e4f5a6c",
"kind": "return",
"status": "returned",
"amount": { "amount": "3000", "currency": "BRL" },
"fee": null,
"settled_at": "2026-10-17T23:10:00Z"
},
{
"object_id": "wdr_0192a3b40c6d7e8f9a0b1c2d3e4f5a6c",
"external_id": "wdr_0192a3b4-5c6d-7e8f-9a0b-1c2d3e4f5a6c",
"kind": "withdrawal",
"status": "paid",
"amount": { "amount": "5000", "currency": "BRL" },
"fee": null,
"settled_at": "2026-10-17T21:30:05Z"
}
],
"next_cursor": null,
"totals": {
"deposits_paid": { "amount": "5000", "currency": "BRL" },
"withdrawals_paid": { "amount": "5000", "currency": "BRL" },
"refunds": { "amount": "0", "currency": "BRL" },
"chargebacks": { "amount": "0", "currency": "BRL" },
"fees": { "amount": "0", "currency": "BRL" },
"count": 3
},
"digest": "140a4741bd77e90442f0d09443876ec084158fe489d13e96529a21909e002d4f"
}
As três linhas canônicas que geram esse digest, já na ordem certa:
dep_0192a3b40c6d7e8f9a0b1c2d3e4f5a6b|dep_0192a3b4-5c6d-7e8f-9a0b-1c2d3e4f5a6b|deposit|paid|5000|BRL|2026-10-17T15:07:40Z
wdr_0192a3b40c6d7e8f9a0b1c2d3e4f5a6c|wdr_0192a3b4-5c6d-7e8f-9a0b-1c2d3e4f5a6c|return|returned|3000|BRL|2026-10-17T23:10:00Z
wdr_0192a3b40c6d7e8f9a0b1c2d3e4f5a6c|wdr_0192a3b4-5c6d-7e8f-9a0b-1c2d3e4f5a6c|withdrawal|paid|5000|BRL|2026-10-17T21:30:05Z
Resposta 409 para um dia que ainda não abriu (Retry-After: 3600):
{
"type": "https://docs.dracma.test/errors/day_open",
"title": "day_open",
"status": 409,
"code": "day_open",
"detail": "disponível a partir de 2026-10-18T02:00:00Z",
"trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
"retryable": true
}
Dia sem movimento (items vazio, digest do texto vazio):
{
"date": "2026-10-16",
"currency": "BRL",
"items": [],
"next_cursor": null,
"totals": {
"deposits_paid": { "amount": "0", "currency": "BRL" },
"withdrawals_paid": { "amount": "0", "currency": "BRL" },
"refunds": { "amount": "0", "currency": "BRL" },
"chargebacks": { "amount": "0", "currency": "BRL" },
"fees": { "amount": "0", "currency": "BRL" },
"count": 0
},
"digest": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
}
Erros e casos de borda¶
| Situação | Resposta |
|---|---|
date inválida, currency ausente, limit ou cursor inválidos |
400 validation_failed, com o campo e o reason em errors[] |
| Moeda não habilitada | 400 currency_not_enabled |
| Dia ainda não aberto | 409 day_open, retryable: true, Retry-After em segundos |
| Livro, assinador ou banco fora | 503 service_unavailable, repetível |
| Digest do livro diferente do recalculado pela API | 503 service_unavailable, e o Dracma é alertado. Nunca entrega um relatório inconsistente |
Sem o escopo read |
403 insufficient_scope |
- Meia-noite UTC. Um item do dia
Dpode ter sido creditado por você perto da meia-noite do seu fuso. Compare pelosettled_at(UTC). - Digest diferente do seu. Confira primeiro a ordenação por
(object_id, kind), o\nfinal e se você reformatousettled_at. - Assinatura que não bate. Confira se está usando o segredo de webhook e se a mensagem é
"<t>." + digest, não o corpo. - Depois de uma rotação do segredo, podem vir dois
v1por um tempo. Basta um bater. kindnovo. Não derrube o seu job: guarde o item e abra revisão.