Webhooks
Webhooks permitem que o Sentinex notifique sua aplicação quando eventos relevantes acontecem: alerta criado, KYC concluído, transação reanalisada. Substitui polling por push.
Eventos disponíveis
Seção intitulada “Eventos disponíveis”| Event type | Quando dispara | Payload principal |
|---|---|---|
customer.created | POST /vasp/customers | objeto customer |
customer.updated | mudança de campo persistido | customer + changed_fields |
customer.deleted | DELETE /vasp/customers/:id (LGPD soft-delete) | { id, deleted_at } |
wallet.linked | POST /vasp/wallets | wallet + customer_id |
wallet.unlinked | desvincular wallet | wallet + razão |
kyc.completed | tier muda para STANDARD ou ENHANCED | customer + kyc_tier + completed_at |
kyc.review_required | Exato/bureau retornou inconsistência | customer + reasons[] |
transaction.analyzed | KYT processou transação | transaction + decision + score |
alert.created | regra disparou alerta novo | alert |
alert.escalated | analista escalou | alert + escalation event |
alert.resolved | analista resolveu | alert + resolution |
case.opened | caso compliance aberto | case |
sar.submitted | COAF SAR enviado | sar metadata |
Wildcard: events: [] (array vazio) na subscription recebe todos.
Configurar subscription
Seção intitulada “Configurar subscription”curl -X POST https://api.crypto.sentinexrisk.com/api/v1/vasp/webhooks \ -H "X-API-Key: $SENTINEX_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "target_url": "https://hooks.suaempresa.com.br/sentinex", "events": ["alert.created", "alert.escalated", "kyc.completed"], "description": "Pager + dashboard ops" }'
# Requer o escopo vasp:webhooks:manage. O secret HMAC é retornado UMA# única vez na resposta 201. A rota /admin/outbound-webhooks/subscriptions# é a equivalente do dashboard (JWT).Resposta 201:
{ "success": true, "data": { "id": "wh_01HVZ…", "url": "https://hooks.suaempresa.com.br/sentinex", "secret": "whsec_<EXEMPLO_NAO_USE_EM_PROD>", "events": ["alert.created", "alert.escalated", "kyc.completed"], "created_at": "2026-05-18T14:32:11Z" }}Assinatura HMAC SHA-256
Seção intitulada “Assinatura HMAC SHA-256”Toda requisição vem com:
POST /sentinex HTTP/1.1Content-Type: application/jsonX-Sentinex-Event: alert.createdX-Sentinex-Timestamp: 1747584000X-Sentinex-Signature: t=1747584000,v1=5d3b7a9e1c4f2d6b8a7e9c3d5f1b2a4e6c8d7f9b1a3e5c7d9b2a4f6e8c1d3b5aX-Sentinex-Delivery: dlv_01HVZ…
{ "event": "alert.created", "data": { ... } }Verificar assinatura
Seção intitulada “Verificar assinatura”A assinatura é HMAC_SHA256(secret, "{timestamp}.{raw_body}").
import crypto from 'crypto';
function verifySentinexWebhook(req, secret) { const sigHeader = req.headers['x-sentinex-signature']; const timestamp = req.headers['x-sentinex-timestamp']; const rawBody = req.rawBody; // string, NÃO o JSON parsed
// Anti-replay: rejeita se mais de 5 min de tolerância if (Math.abs(Date.now() / 1000 - parseInt(timestamp)) > 300) { throw new Error('Timestamp fora da janela'); }
const parts = Object.fromEntries(sigHeader.split(',').map(p => p.split('='))); const expected = crypto .createHmac('sha256', secret) .update(`${timestamp}.${rawBody}`) .digest('hex');
// timingSafeEqual previne timing attack const ok = crypto.timingSafeEqual( Buffer.from(parts.v1, 'hex'), Buffer.from(expected, 'hex'), ); if (!ok) throw new Error('Assinatura inválida');}func verifySentinexWebhook(r *http.Request, secret string) error { sig := r.Header.Get("X-Sentinex-Signature") ts := r.Header.Get("X-Sentinex-Timestamp")
tsInt, _ := strconv.ParseInt(ts, 10, 64) if math.Abs(float64(time.Now().Unix()-tsInt)) > 300 { return errors.New("timestamp fora da janela") }
body, _ := io.ReadAll(r.Body) mac := hmac.New(sha256.New, []byte(secret)) mac.Write([]byte(ts + "." + string(body))) expected := hex.EncodeToString(mac.Sum(nil))
parts := strings.Split(sig, ",") var got string for _, p := range parts { if strings.HasPrefix(p, "v1=") { got = strings.TrimPrefix(p, "v1=") } } if !hmac.Equal([]byte(got), []byte(expected)) { return errors.New("assinatura inválida") } return nil}import hmac, hashlib, time
def verify_sentinex_webhook(headers, raw_body: bytes, secret: str): sig = headers["X-Sentinex-Signature"] ts = int(headers["X-Sentinex-Timestamp"])
if abs(time.time() - ts) > 300: raise ValueError("Timestamp fora da janela")
parts = dict(p.split("=") for p in sig.split(",")) expected = hmac.new( secret.encode(), f"{ts}.".encode() + raw_body, hashlib.sha256, ).hexdigest()
if not hmac.compare_digest(parts["v1"], expected): raise ValueError("Assinatura inválida")Política de retry
Seção intitulada “Política de retry”| Tentativa | Atraso após falha anterior |
|---|---|
| 1ª | imediata |
| 2ª | 10s |
| 3ª | 60s |
| 4ª | 5min |
| Após 3 falhas | move para DLQ, status = dead |
Falha = resposta HTTP fora de 2xx OU timeout > 5s.
Após 5 falhas consecutivas em uma subscription, ela é auto-pausada (active: false) para prevenir spam. Você recebe email + evento subscription.paused.
DLQ (Dead Letter Queue)
Seção intitulada “DLQ (Dead Letter Queue)”Eventos que falharam definitivamente ficam disponíveis em:
GET /api/v1/admin/outbound-webhooks/subscriptions/{id}/dlqReplay manual:
POST /api/v1/admin/outbound-webhooks/subscriptions/{id}/dlq/{delivery_id}/replayRotação de secret
Seção intitulada “Rotação de secret”POST /api/v1/admin/outbound-webhooks/subscriptions/{id}/rotateRetorna novo secret. O antigo continua válido por 24h para você atualizar consumers sem downtime. Após 24h, antigo é revogado.
Boas práticas no consumer
Seção intitulada “Boas práticas no consumer”- Responda 2xx em <2s: processamento pesado deve ir para fila assíncrona
- Idempotência por
X-Sentinex-Delivery: o mesmo evento pode chegar duas vezes (rede falha entre nossa app e seu consumer) - Endpoint HTTPS obrigatório (HTTP recusado pelo Sentinex)
- Sem autenticação extra: a assinatura HMAC já é a auth
- Logue
X-Sentinex-Delivery: facilita o suporte cruzar logs
Testar localmente
Seção intitulada “Testar localmente”Use ngrok ou similar e configure subscription apontando para o tunnel:
ngrok http 4000# https://abc123.ngrok.io → seu localhost:4000No console Webhooks → Test Delivery você pode disparar evento sintético sem causar efeito de negócio.