Reconciliação¶
Estado:
GET /reconciliationeGET /balancesestão implementadas (conferido no código em 2026-09-27). A assinatura do digest é feita pelo serviçodracma_webhooks, com o mesmo formato do §4. A fonte dos itens é o fechamento do livro do Dracma (o dono do "liquidado"); a API recalcula o digest e, se ele divergir do livro, responde503(e alerta) em vez de entregar um relatório inconsistente.
A reconciliação prova, todos os dias, que o seu livro e o Dracma contam o mesmo dinheiro. São duas rotinas:
- Diária: o relatório de movimentos liquidados em cada dia UTC, por moeda, com um digest assinado.
- A cada 5 minutos:
GET /events?after=para recuperar webhooks perdidos (ver webhooks.md §8).
1. GET /v1/reconciliation¶
GET /v1/reconciliation?date=2026-09-26¤cy=BRL&limit=200
GET /v1/reconciliation?date=2026-09-26¤cy=BRL&limit=200&cursor=<next_cursor>
| Parâmetro | Obrigatório | Conteúdo |
|---|---|---|
date |
sim | Dia UTC (AAAA-MM-DD). Formato inválido → 400 (reason: format). |
currency |
sim | Moeda da carteira (BRL no lançamento). Ausente → 400 (required); não habilitada → 400 currency_not_enabled. |
cursor |
não | O valor de next_cursor da página anterior. |
limit |
não | De 1 a 200. O padrão é 50. |
- Quando está disponível: a partir de 02:00 UTC do dia seguinte. Antes disso a resposta é 409
day_open, comretryable: trueeRetry-After= segundos até a abertura. Esse erro significa "tente mais tarde": agende para depois das 02:00 UTC, não repita em laço. - Paginado.
totalsedigestsó vêm na última página, que é a que temnext_cursor: null. Nas outras páginas os dois vêmnull. - Rate limit: o contrato fala em 5 req/s; hoje a rota usa o balde geral de leitura (100 req/s por chave). Pagine devagar mesmo assim (D-A2 em divergencias.md).
- Livro, assinador (
dracma_webhooks) ou banco fora →503 service_unavailableretryable. A última página nunca sai sem assinatura. - Escopo:
read.
Resposta (última página):
{
"date": "2026-09-26",
"currency": "BRL",
"items": [
{"object_id": "dep_01J9ZQ3", "external_id": "dep_0192a3b4-5c6d-7e8f-9a0b-1c2d3e4f5a6b",
"kind": "deposit", "status": "paid", "amount": {"amount": "5000", "currency": "BRL"},
"fee": null, "settled_at": "2026-09-26T15:00:00Z"}
],
"next_cursor": null,
"totals": {
"deposits_paid": {"amount": "5000", "currency": "BRL"},
"withdrawals_paid": {"amount": "0", "currency": "BRL"},
"refunds": {"amount": "0", "currency": "BRL"},
"chargebacks": {"amount": "0", "currency": "BRL"},
"fees": {"amount": "0", "currency": "BRL"},
"count": 1
},
"digest": "<sha256 hex>"
}
A última página traz também o header Dracma-Signature: t=<ts>,v1=<hex>[,v1=<hex>].
kind¶
kind |
O que é |
|---|---|
deposit |
Depósito liquidado (paid). |
withdrawal |
Saque liquidado (paid). |
refund |
Estorno de depósito. |
chargeback |
Chargeback ou MED. |
fee |
Tarifa. |
return, recall, receipt |
Da v1.1.0: transferência bancária devolvida, recall aceito e recebimento não casado. Só aparecem com wire ou SWIFT, que ainda não estão disponíveis. Aplique o leitor tolerante: um kind desconhecido entra no digest como qualquer outro, mas não gera lançamento automático no seu livro; vira item de revisão. |
2. 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_idem ordem crescente (comparação de bytes, sem locale). Empate (o mesmo objeto em dois itens, ex.:depositerefunddo mesmo depósito): ordene porkindcrescente. É o que o código do Dracma faz; o contrato só citaobject_id(D-C1 em divergencias.md). - 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 campo
digestda última página.
Conferido no código (internal/application/reconciliation):
amounté o inteiro na menor unidade exatamente como vem no JSON ("5000", não50.00);currencyda linha é a moeda do item, igual aamount.currencye aocurrencydo topo da resposta;settled_até RFC 3339 UTC com segundos, sem fração (2026-09-26T15:00:00Z), igual à string do JSON: use a string como veio, sem reformatar;- dia sem movimento:
items: [],digest= sha256 de""(e3b0c442…b855).
3. A assinatura do digest¶
Dracma-Signature: t=<ts>,v1=hex( HMAC-SHA256( segredo_de_webhook, "<t>." + digest ) )
- O segredo é o segredo de webhook (não o de chamada), com a mesma rotação do webhook: podem vir
dois
v1, e basta um bater. - A mensagem assinada é o
t, um ponto e o digest em hex (os 64 caracteres), não o corpo da resposta. - Verifique no momento do download (os exemplos aplicam a mesma janela de ±300 s do webhook sobre o
t=): o Dracma assina a cada pedido da última página, então ot=é sempre recente. O contrato não diz se a janela vale para o digest; verificar na hora funciona nas duas leituras. Guarde o header junto com o relatório como prova — uma reverificação posterior compara só ov1, sem a janela.
Pseudocódigo:
itens = []
cursor = nulo
repita:
r = GET /v1/reconciliation?date=D¤cy=BRL&limit=200[&cursor=cursor] # sua credencial
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)
A primitiva é a mesma do webhook ("<t>." + mensagem). Os clientes em exemplos/
reaproveitam verificarWebhook passando o digest no lugar do corpo.
4. O que comparar com o seu livro¶
Para cada dia e cada moeda:
- Todo
deposit/paiddo Dracma tem exatamente um crédito no seu livro, com o mesmoexternal_ide o mesmo valor. - Todo crédito seu tem um
deposit/paidno relatório. Um crédito sem par é o caso mais grave: é dinheiro criado sem liquidação. - Todo
withdrawal/paidcorresponde a uma reserva sua que foi finalizada, e vice-versa. refundechargebackcorrespondem às decisões que você registrou (ver depositos.md).- Os totais batem com a soma dos itens.
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 /events?object_id=) e registre. A regra de
alerta que o go-live-operador.md exige é disparar quando o digest não bate, quando a
assinatura não bate ou quando falta um par.
Dica: um item no relatório do dia D pode ter sido creditado por você perto da meia-noite UTC do dia
D − 1 ou D + 1, por causa do fuso local. Compare pelo settled_at do Dracma (UTC), não pela sua data
local.
5. Caixa da operação: GET /v1/balances¶
GET /v1/balances devolve, por moeda, os valores available, pending_in, pending_out e as_of. É o
caixa de liquidação que paga os saques. Um saque sem caixa recebe 422 insufficient_operation_balance
(ver saques.md). Monitore esse valor e alerte antes de ele acabar.
6. Rotina recomendada¶
| Quando | O quê |
|---|---|
| A cada 5 min | GET /events?after=<último evt> → dedup, fetch-back e efeito (ver webhooks.md §8). |
| Diariamente, depois das 02:00 UTC | GET /reconciliation do dia anterior, para cada moeda → digest, assinatura e comparação com o livro. |
| Continuamente | GET /balances → alerta de caixa baixo. |