🔔 Eventos Assíncronos (Webhooks)
Várias operações financeiras dependem de confirmação de redes externas (Pix, blockchain, compliance) e não ocorrem instantaneamente. A DEX API envia notificações automáticas do tipo push para o seu servidor quando ocorrem atualizações.
É como o rastreamento de uma entrega. Em vez de entrar no site da transportadora a cada 5 minutos, você recebe um SMS avisando que o pacote foi entregue.
O Que É um Webhook?
Um webhook é apenas um endpoint do seu servidor (uma URL POST) que você cadastra no Painel do Participante ou na interface do usuário (no caso de contas pessoais).
1. Dicionário de Eventos Comuns
PIX_DEPOSIT_COMPLETED: Um depósito via Pix foi recebido e creditado.PIX_WITHDRAW_COMPLETED: Um saque via Pix foi liquidado no Banco Central.PIX_WITHDRAW_FAILED: O saque Pix falhou e foi estornado.KYC_APPROVED: A verificação de documentos cadastrais do usuário foi aprovada.KYC_REJECTED: Documentos rejeitados pelo time de compliance (requer reenvio).ACCOUNT_UPGRADED_TO_FULL: O usuário foi promovido ao nívelFULLe pode transacionar.CRYPTO_DEPOSIT_CONFIRMED: Depósito em criptomoeda recebido e minerado na blockchain.
2. Formato do Envelope JSON
Todas as notificações chegam na sua URL com o mesmo envelope — id, event, timestamp e data. O id é o identificador da entrega (o mesmo do cabeçalho X-Delivery-Id) e o data varia conforme o evento.
**Todo data traz o campo **userId — o UUID do usuário titular do evento. É ele que diz de quem é o fato notificado. Sem isso, um webhook de participante (que recebe eventos de todos os usuários vinculados) seria indistinguível de qualquer outro.
{
"id": "0df83749-d7b8-4d56-8c43-1e5828453cc1",
"event": "PIX_WITHDRAW_COMPLETED",
"timestamp": "2026-06-17T15:00:00.000Z",
"data": {
"userId": "550e8400-e29b-41d4-a716-446655440000",
"amount": "10000",
"grossAmount": "10250",
"e2eId": "E00416968202601211957HWt6sTkLPfw",
"pixKey": "destinatario@example.com",
"transactionDate": "2026-06-17T15:00:00.000Z",
"ledgerTransactionId": "3f6a2c81-9d47-4e52-a1b8-6c0e5f39d724"
}
}
O userId é o mesmo identificador devolvido pelo Portal de Leitura (GET /v1/participant/users e os endpoints /v1/participant/users/{userId}/...). Use-o como chave de correlação entre o webhook e o usuário que você já conhece — não há tradução de identidade a fazer.
O data não traz um campo status: o próprio event já diz o que aconteceu (PIX_WITHDRAW_COMPLETED versus PIX_WITHDRAW_FAILED). Também não existe transactionId — o identificador da movimentação no nosso ledger é o ledgerTransactionId.
No saque, a tarifa é cobrada por fora: o amount é o que chegou ao destinatário e o grossAmount é o que saiu da conta do usuário (grossAmount ≥ amount). Nos eventos Pix, os valores vêm como strings em centavos, nunca como números.
No caso de PIX_DEPOSIT_COMPLETED, o data traz também o qrCodeId do QR Code que originou o depósito:
{
"id": "7c2f1b90-3a4e-4c11-9f77-2a1d5e0b8c33",
"event": "PIX_DEPOSIT_COMPLETED",
"timestamp": "2026-06-17T15:04:22.000Z",
"data": {
"userId": "550e8400-e29b-41d4-a716-446655440000",
"qrCodeId": "550e8400-e29b-41d4-a716-446655440000",
"amount": "24500",
"grossAmount": "25000",
"e2eId": "E00416968202601211957HWt6sTkLPfw",
"ledgerTransactionId": "9b1c7d42-58e0-4a3b-b6f1-0d7c9e2a4f18"
}
}
Eventos de KYC
Os eventos KYC_SUBMITTED, KYC_RESUBMITTED, KYC_APPROVED, KYC_REJECTED e KYC_REVOKED são os que mais dependem do userId: eles não têm outro identificador de usuário no data. KYC_APPROVED, por exemplo, chega assim — só o titular, sem mais nenhum campo:
{
"id": "b41e6d02-9f38-4a17-8c5e-2d0f7a91be44",
"event": "KYC_APPROVED",
"timestamp": "2026-06-17T15:10:00.000Z",
"data": {
"userId": "550e8400-e29b-41d4-a716-446655440000"
}
}
KYC_REJECTED e KYC_REVOKED acrescentam reason (texto voltado ao usuário) ao userId. KYC_SUBMITTED e KYC_RESUBMITTED acrescentam submissionId e documentCount.
O userId sempre reflete o titular real do evento, nunca um valor enviado pelo solicitante — não é possível forjá-lo pelo corpo da requisição que originou o fato.
O qrCodeId é o mesmo valor devolvido no POST /pix/deposit que gerou a cobrança. Use-o para correlacionar a confirmação com a cobrança original — sem ele não há como ligar o webhook ao pedido de depósito que você criou. Aqui o amount é o valor líquido creditado (já descontadas as taxas) e o grossAmount é o valor bruto pago pelo pagador — relação inversa à do saque.
Cada chamada inclui o cabeçalho X-Signature para validação HMAC.
3. Validação de Assinatura (Segurança HMAC)
Para garantir que a notificação foi enviada pela Ether (e não por um invasor simulando depósitos), você deve validar a assinatura usando o seu Webhook Secret:
- Extraia o timestamp
te o hashv1do cabeçalhoX-Signature(ex:t=1718640000,v1=a1b2c3d...). - Concatene em string:
t.X-Delivery-Id.Corpo_JSON_Cru(separados por ponto). - Gere um hash HMAC-SHA256 usando seu Webhook Secret.
- Compare com a assinatura
v1recebida. Rejeite assinaturas com mais de 5 minutos de diferença (proteção anti-replay).
Exemplo: Servidor Webhook Mínimo (Node.js/Express)
Veja abaixo um exemplo funcional completo de servidor para receber e autenticar webhooks de forma segura:
import express from 'express';
import crypto from 'crypto';
const app = express();
app.use(express.raw({ type: 'application/json' }));
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET || 'seu_secret';
function validar(body: Buffer, deliveryId: string, signature: string): boolean {
const partes = signature.split(',');
const timestamp = partes[0]?.replace('t=', '');
const hash = partes[1]?.replace('v1=', '');
if (!timestamp || !hash) return false;
const agora = Math.floor(Date.now() / 1000);
if (Math.abs(agora - Number(timestamp)) > 300) return false; // Replay protect
const payload = `${timestamp}.${deliveryId}.${body.toString()}`;
const hashEsperado = crypto.createHmac('sha256', WEBHOOK_SECRET).update(payload).digest('hex');
return crypto.timingSafeEqual(Buffer.from(hash, 'hex'), Buffer.from(hashEsperado, 'hex'));
}
app.post('/webhooks/dex', (req, res) => {
const deliveryId = req.headers['x-delivery-id'] as string;
const signature = req.headers['x-signature'] as string;
if (!validar(req.body, deliveryId, signature)) {
return res.status(401).send('Assinatura inválida');
}
const evento = JSON.parse(req.body.toString());
console.log(`Evento recebido: ${evento.event}`);
res.status(200).send('OK');
});
app.listen(3001);

