Pular para o conteúdo

Webhooks

Webhooks permitem que a Sentinex Risk avise a sua aplicação quando algo acontece no seu tenant, em vez de você consultar a API em intervalos. A subscription é gerenciada pela própria chave de API, em /vasp/webhooks, com o escopo vasp:webhooks:manage.

Estes são os eventos que o código de produção emite numa integração de KYT:

Evento Quando é emitido
kyt.screening.completed Uma análise de POST /vasp/screenings foi concluída e gravada. A repetição idempotente devolve a análise original e não emite de novo
kyt.screening.sanction_hit A mesma análise teve sanctions.hit verdadeiro. Chega junto do kyt.screening.completed
kyt.case.client_decision.recorded A decisão do cliente foi registrada, por PATCH /vasp/screenings/{id}/client-decision ou pela fila de análise do painel
customer.created POST /vasp/customers criou um cliente
wallet.linked POST /vasp/wallets vinculou uma carteira, nova ou já conhecida do tenant, ou PATCH /vasp/wallets/{id} reativou uma carteira desativada
wallet.deactivated DELETE /vasp/wallets/{id} desativou uma carteira
alert.created Um alerta foi criado para o tenant, qualquer que seja a origem: análise sinalizada, matrícula de carteira em revisão, monitoramento contínuo ou detector agendado. Sai depois que a gravação do alerta foi confirmada, um evento por alerta, e alerta desfeito na mesma operação não gera evento
alert.resolved Um alerta do tenant foi resolvido no painel

O evento de screening notifica por referência, com payload reduzido do catálogo: a conclusão carrega screening_ref, recommendation, risk_band, sanctions_match e completed_at; o evento de sanção carrega screening_ref, risk_band, match_scope e completed_at. Endereço, hash, ativo, score numérico, regras e customer_ref não são enviados nesses eventos. Use GET /vasp/screenings/{id} para consultar a evidência com autenticação. Não use o evento como substituto da resposta completa do screening.

O payload de alert.created contém alert_ref, category_code, severity, status e created_at. O prazo, score numérico, regra, descrição, metadata, tags e evidências ficam na consulta autenticada de alertas. Use alert_ref para correlacionar com o id de GET /vasp/alerts; não espere alert_id nesse payload de notificação.

Além dos eventos de negócio, existe o webhook.test, entregue apenas quando você chama POST /vasp/webhooks/{id}/test. Ele não pode ser assinado na lista events e não indica efeito de negócio.

Todos os eventos: events: [] (lista vazia) assina todos os nomes aceitos.

O secret HMAC é gerado por você e enviado no corpo. O servidor guarda o secret cifrado e um resumo SHA-256 dele, e nunca o devolve: não existe rota que o recupere nem que o rotacione.

Janela do terminal
curl -X POST https://crypto-api.sentinexrisk.com/api/v1/vasp/webhooks \
-H "X-API-Key: $SENTINEX_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"target_url": "https://hooks.demovasp.com.br/sentinex",
"events": ["kyt.screening.completed", "kyt.screening.sanction_hit", "kyt.case.client_decision.recorded"],
"description": "Mesa de risco",
"secret": "<de 32 a 128 bytes gerados por você, por exemplo: openssl rand -hex 32>"
}'
Campo Regra conferida no servidor
target_url HTTPS obrigatório, com host, sem usuário e senha na URL, sem query string e sem fragmento. Violação responde 400 com target_url must be valid HTTPS URL
secret De 32 a 128 bytes. Fora da faixa responde 400 com secret must contain 32 to 128 bytes
events Cada item precisa estar na lista aceita. Item fora dela responde 400 com invalid event: <nome>. Lista vazia assina todos
description Opcional, texto livre

Resposta 201, sem envelope data e sem o secret:

{
"success": true,
"id": "3f0d1e42-8f8e-4a1c-9b7a-2c5d7e9f1a3b",
"target_url": "https://hooks.demovasp.com.br/sentinex",
"events": ["kyt.screening.completed", "kyt.screening.sanction_hit", "kyt.case.client_decision.recorded"]
}

GET /vasp/webhooks lista as suas subscriptions com active, consecutive_failures, last_success_at e last_failure_at, e DELETE /vasp/webhooks/{id} apaga uma delas.

Os eventos de negócio só são entregues quando o tenant é um cliente em produção: ativo, do tipo cliente, fora de demonstração e de dados sintéticos, no modo live, faturável e com efeitos externos liberados. Num tenant que não atende a essas condições, a entrega é encerrada sem chegar à sua URL e sem contar como falha da subscription. O webhook.test é a exceção: ele é entregue em qualquer tenant, justamente para você validar o receptor antes do tráfego real.

Toda entrega chega assim:

POST /sentinex HTTP/1.1
Content-Type: application/json
User-Agent: Sentinex-Webhook/1.0
X-Sentinex-Signature: 5d3b7a9e1c4f2d6b8a7e9c3d5f1b2a4e6c8d7f9b1a3e5c7d9b2a4f6e8c1d3b5a
X-Sentinex-Timestamp: 1758808800
X-Sentinex-Event-Type: kyt.screening.completed
X-Sentinex-Event-Id: 7c9e6679-7425-40de-944b-e07fc1f90ae7
X-Sentinex-Delivery-Id: 0a1b2c3d-4e5f-4a6b-8c9d-0e1f2a3b4c5d
X-Sentinex-Attempt: 1
{"attempt":1,"delivery_id":"0a1b2c3d-4e5f-4a6b-8c9d-0e1f2a3b4c5d","event_id":"7c9e6679-7425-40de-944b-e07fc1f90ae7","event_type":"kyt.screening.completed","payload":{},"subscription_id":"3f0d1e42-8f8e-4a1c-9b7a-2c5d7e9f1a3b","tenant_id":"a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d"}
Header Conteúdo
X-Sentinex-Signature HMAC SHA-256 em hex puro, minúsculo, 64 caracteres. Sem prefixo t=, sem v1=, sem lista de versões
X-Sentinex-Timestamp Unix epoch em segundos, gerado no momento de cada tentativa
X-Sentinex-Event-Type Nome do evento, igual ao campo event_type do corpo
X-Sentinex-Event-Id UUID do evento. Estável entre tentativas
X-Sentinex-Delivery-Id UUID da entrega. Estável entre tentativas
X-Sentinex-Attempt Número da tentativa desta entrega. Começa em 1 e aumenta nas retentativas

O corpo é um envelope de sete campos. O conteúdo do evento vem dentro de payload, não de data.

Campo Tipo Nota
event_type string Nome do evento
event_id string (UUID) Estável entre tentativas
delivery_id string (UUID) Estável entre tentativas
subscription_id string (UUID) Subscription que originou a entrega
tenant_id string (UUID) Seu tenant
attempt inteiro Começa em 1 e sobe a cada retentativa
payload objeto Conteúdo do evento

Deduplique pelo par event_id mais delivery_id. Não deduplique pelo corpo inteiro, porque attempt muda entre tentativas e, com ele, a assinatura.

A assinatura é hex(HMAC_SHA256(secret, timestamp + "." + corpo_cru)), calculada sobre os bytes crus do corpo, antes de qualquer parsing, com o valor exato do header X-Sentinex-Timestamp. Compare em tempo constante e contra a string hex inteira. O servidor não impõe janela de tempo do seu lado: a tolerância de 5 minutos dos exemplos é uma defesa contra reenvio que você aplica no seu receptor.

import crypto from 'node:crypto';
function verificarWebhookSentinex(headers, corpoCru, secret) {
const assinatura = headers['x-sentinex-signature'];
const timestamp = headers['x-sentinex-timestamp'];
if (typeof assinatura !== 'string' || typeof timestamp !== 'string') {
throw new Error('Headers de assinatura ausentes');
}
if (!/^[0-9]+$/.test(timestamp) || !Number.isSafeInteger(Number(timestamp)) ||
Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
throw new Error('Timestamp fora da janela');
}
const esperada = crypto
.createHmac('sha256', secret)
.update(`${timestamp}.`)
.update(corpoCru) // Buffer com os bytes recebidos, não o JSON interpretado
.digest('hex');
const recebida = Buffer.from(assinatura, 'utf8');
const calculada = Buffer.from(esperada, 'utf8');
if (recebida.length !== calculada.length || !crypto.timingSafeEqual(recebida, calculada)) {
throw new Error('Assinatura inválida');
}
}

Uma entrega é considerada falha quando a resposta sai da faixa 2xx, quando há erro de transporte ou quando a sua URL não responde em 10 segundos. São até 10 tentativas no total, com espera crescente depois de cada falha:

Depois da falha Próxima tentativa em
1ª 2 minutos
2ª 4 minutos
3ª 8 minutos
4ª 16 minutos
5ª 32 minutos
6ª a 9ª 60 minutos cada
10ª a entrega vira dead e não é mais tentada

Depois de 10 falhas consecutivas na mesma subscription, ela é pausada (active: false) e as entregas pendentes dela passam a dead sem novo envio. Um sucesso zera o contador. A pausa não gera evento nem e-mail: acompanhe active e consecutive_failures em GET /vasp/webhooks. Por chave de API não existe reativação: apague a subscription pausada e crie outra depois de corrigir o receptor.

O reenvio de uma entrega em failed ou dead não faz parte do contrato de chave de API. Ele é feito numa sessão do painel, por um administrador do tenant.

Não há rotação nem período de convivência entre secrets. Para trocar o secret, crie uma subscription nova com o secret novo, valide as duas em paralelo no seu receptor e só então apague a antiga com DELETE /vasp/webhooks/{id}.

POST /vasp/webhooks/{id}/test enfileira um webhook.test para uma subscription ativa e responde 202 com success, subscription_id, event_id e status: "pending" na raiz, sem envelope data. Ele passa pelo mesmo mecanismo de entrega, pela mesma retentativa e pela mesma assinatura HMAC das entregas reais. Subscription pausada responde 404.

Para receber no seu ambiente local, exponha a porta por um túnel HTTPS e aponte a subscription para ele.

  1. Responda 2xx rápido: o limite é de 10 segundos por tentativa, e processamento pesado deve ir para uma fila sua
  2. Deduplique por X-Sentinex-Delivery-Id mais X-Sentinex-Event-Id: a entrega é pelo menos uma vez, e o mesmo evento pode chegar duas vezes
  3. Valide a assinatura em toda entrega, antes de ler o corpo como JSON
  4. Registre X-Sentinex-Delivery-Id no seu log, para o suporte cruzar a entrega com o nosso registro

KYC próprio e eventos kyc.* não estão disponíveis no produto KYT atual. Nas rotas B2B de cases /api/v1/kyc/*, após autenticação e acesso da conta, o gate responde 403 PRODUCT_MODULE_DISABLED. Contratar KYC exige produto habilitado e escopos próprios, sem data prometida. Assinar um nome aceito não habilita seu emissor.