Webhooks
Receba alertas, scripts novos e relatórios prontos no seu endpoint, verifique a assinatura HMAC e trate as retentativas.
Com um webhook, o Proteside envia um POST para o seu endpoint assim que algo acontece: um alerta novo, um script
desconhecido no checkout, um relatório PCI DSS pronto. Você não precisa ficar consultando a API.
Webhooks, Slack, Microsoft Teams e e-mail são todos canais de notificação da mesma organização. O que você cria pela API aparece em Configurações → Notificações, e vice-versa.
Criar um webhook
Abra Configurações → Notificações e clique em Adicionar canal. É preciso ser proprietário ou administrador.
Escolha o tipo Webhook e o escopo: Esta loja (só eventos da loja selecionada) ou Organização (eventos de todas as lojas). Tipo e escopo não podem ser alterados depois.
Preencha Nome e URL (precisa começar com https://) e deixe Formato em JSON (assinado).
Preencha Segredo de assinatura com um valor aleatório de 16 a 128 caracteres e guarde-o: você vai precisar dele para verificar as entregas.
Escolha a Severidade mínima e os Eventos, mantenha Canal ativo ligado e clique em Salvar alterações.
Sem segredo, sem assinatura
Pelo dashboard o segredo é opcional. Se o campo ficar vazio, as entregas saem sem o header
X-Proteside-Signature e o card do canal mostra "sem assinatura". Sempre preencha o segredo em webhooks JSON.
Eventos
| Evento | Quando é enviado | severity | Filtrado por min_severity |
|---|---|---|---|
alert.created | Um alerta novo é aberto (primeira ocorrência), inclusive violações de CSP | a do alerta | sim |
alert.resolved | Um alerta de SDK silencioso é resolvido sozinho porque o SDK voltou a enviar eventos | medium | sim |
integrity.changed | O conteúdo de um script autorizado mudou (alerta SCRIPT_INTEGRITY_MISMATCH) | high | sim |
header.changed | Um header de segurança da página de pagamento mudou (alerta HEADER_CHANGED) | medium | sim |
sdk.silent | Uma loja com tráfego ficou 24 horas sem enviar eventos do SDK (alerta SDK_SILENT) | medium | sim |
script.detected | Um script novo apareceu no checkout e aguarda revisão | info | não |
policy.applied | Uma política de aprovação decidiu um script (modo automático ou Aplicar) | info | não |
report.ready | O relatório PCI DSS agendado ficou pronto (segunda-feira, 09:00 UTC) | info | não |
test | Você disparou um teste | info | ignora todos os filtros |
Eventos do catálogo que hoje não são enviados
script.authorized e script.blocked aparecem na lista de eventos do dashboard e são aceitos no campo events,
mas o Proteside não os envia hoje. Para acompanhar autorizações e bloqueios, consulte GET /scripts ou a auditoria.
alert.resolved só é enviado na resolução automática de SDK silencioso. Resolver um alerta pelo dashboard ou por
PATCH /alerts/{id} não gera evento. Os relatórios gerados por GET /reports/pci também não geram report.ready:
só o envio agendado gera.
Filtros do canal
Cada canal tem dois filtros:
events: lista de eventos aceitos. Vazio ounullsignifica todos.min_severity: vale só para os eventos de alerta (alert.created,alert.resolved,integrity.changed,header.changedesdk.silent).script.detected,policy.appliedereport.readypassam sempre.
O padrão high descarta eventos medium
Canais novos nascem com min_severity: high (Alto ou superior). Com esse valor, sdk.silent, header.changed e
alertas medium ou low (como violações de CSP) nunca chegam. Se quer saber quando o SDK parou de rodar, use
min_severity: "medium" (Médio ou superior) ou menor.
Formato da entrega
O Proteside faz um POST com corpo JSON e estes headers:
| Header | Conteúdo |
|---|---|
Content-Type | application/json |
User-Agent | Proteside-Webhooks/1.0 |
X-Proteside-Event | Tipo do evento, por exemplo alert.created |
X-Proteside-Delivery | ID da entrega (único por canal e evento, igual em todas as retentativas) |
X-Proteside-Signature | t=<unix>,v1=<hex> (presente quando o canal tem segredo) |
Todo evento usa o mesmo envelope:
Propriedade
Tipo
Exemplos de payload
{
"id": "evt_5d41402abc4b2a76b9719d911017c592",
"type": "alert.created",
"created_at": "2026-10-06T09:40:01.123Z",
"org_id": "7f3c1e2a-0b4d-4c8e-9f10-2a3b4c5d6e7f",
"store_id": "0c1d2e3f-1111-4222-8333-444455556666",
"severity": "critical",
"data": {
"alert": {
"id": "e1f2a3b4-8888-4999-8000-111122223333",
"type": "PIX_TAMPERED",
"severity": "critical",
"occurrences": 1,
"page": "/checkout/pagamento",
"details": { "reason": "untrusted_recipient", "method": "pix" },
"first_seen_at": "2026-10-06T09:40:00.000Z",
"source": "sdk",
"url": "https://app.proteside.com/alerts/e1f2a3b4-8888-4999-8000-111122223333"
},
"store": {
"id": "0c1d2e3f-1111-4222-8333-444455556666",
"name": "Minha Loja",
"domain": "minhaloja.com.br"
}
}
}alert.resolved, integrity.changed, header.changed e sdk.silent usam o mesmo formato de data.alert.
{
"id": "evt_7c9e6679f4e14b2a8f0e2d3c4b5a6978",
"type": "integrity.changed",
"created_at": "2026-10-06T10:15:02.000Z",
"org_id": "7f3c1e2a-0b4d-4c8e-9f10-2a3b4c5d6e7f",
"store_id": "0c1d2e3f-1111-4222-8333-444455556666",
"severity": "high",
"data": {
"alert": {
"id": "c3d4e5f6-2222-4333-8444-555566667777",
"type": "SCRIPT_INTEGRITY_MISMATCH",
"severity": "high",
"occurrences": 1,
"page": "/",
"details": {
"scriptId": "b2c3d4e5-6666-4777-8888-999900001111",
"src": "https://cdn.fornecedor.example/widget.js",
"authorizedHashPrefix": "9b74c9897bac770f",
"currentHashPrefix": "1f3870be274f6c49",
"hashSource": "verifier",
"previousStatus": "authorized"
},
"first_seen_at": "2026-10-06T10:15:00.000Z",
"source": "system",
"url": "https://app.proteside.com/alerts/c3d4e5f6-2222-4333-8444-555566667777"
},
"store": { "id": "0c1d2e3f-1111-4222-8333-444455556666", "name": "Minha Loja", "domain": "minhaloja.com.br" }
}
}{
"id": "evt_0a1b2c3d4e5f60718293a4b5c6d7e8f9",
"type": "sdk.silent",
"created_at": "2026-10-06T11:00:04.000Z",
"org_id": "7f3c1e2a-0b4d-4c8e-9f10-2a3b4c5d6e7f",
"store_id": "0c1d2e3f-1111-4222-8333-444455556666",
"severity": "medium",
"data": {
"alert": {
"id": "d4e5f6a7-3333-4444-8555-666677778888",
"type": "SDK_SILENT",
"severity": "medium",
"occurrences": 1,
"page": "/",
"details": {
"lastEventAt": "2026-10-05T10:58:00.000Z",
"silentHours": 24,
"domain": "minhaloja.com.br",
"host": "minhaloja.com.br",
"page": "/"
},
"first_seen_at": "2026-10-06T11:00:00.000Z",
"source": "system",
"url": "https://app.proteside.com/alerts/d4e5f6a7-3333-4444-8555-666677778888"
},
"store": { "id": "0c1d2e3f-1111-4222-8333-444455556666", "name": "Minha Loja", "domain": "minhaloja.com.br" }
}
}{
"id": "evt_9f86d081884c7d659a2feaa0c55ad015",
"type": "script.detected",
"created_at": "2026-10-06T09:12:30.000Z",
"org_id": "7f3c1e2a-0b4d-4c8e-9f10-2a3b4c5d6e7f",
"store_id": "0c1d2e3f-1111-4222-8333-444455556666",
"severity": "info",
"data": {
"script": {
"id": "f6a7b8c9-4444-4555-8666-777788889999",
"src": "https://cdn.novo-fornecedor.example/tag.js",
"type": "unknown",
"category": null,
"status": "needs_review",
"vendor_id": null,
"size_bytes": 18234,
"gtm_container": null,
"first_seen_at": "2026-10-06T09:12:29.000Z"
},
"store": { "id": "0c1d2e3f-1111-4222-8333-444455556666", "name": "Minha Loja", "domain": "minhaloja.com.br" }
}
}{
"id": "evt_2c26b46b68ffc68ff99b453c1d304134",
"type": "policy.applied",
"created_at": "2026-10-06T09:13:00.000Z",
"org_id": "7f3c1e2a-0b4d-4c8e-9f10-2a3b4c5d6e7f",
"store_id": "0c1d2e3f-1111-4222-8333-444455556666",
"severity": "info",
"data": {
"script": {
"id": "b2c3d4e5-6666-4777-8888-999900001111",
"src": "https://www.googletagmanager.com/gtm.js",
"status": "authorized",
"type": "known_third",
"category": "tag_manager"
},
"policy": { "id": "a1b2c3d4-5555-4666-8777-888899990000", "name": "Analytics do catálogo", "version": 1 },
"action": "authorize",
"justification": "Tag manager do marketing, sem acesso a campos de pagamento.",
"expires_at": "2027-01-04T09:13:00.000Z",
"actor": "api:ps_live_Ab3dEf6hIj9kLm2nOp5qRs8t",
"store": { "id": "0c1d2e3f-1111-4222-8333-444455556666", "name": "Minha Loja", "domain": "minhaloja.com.br" }
}
}{
"id": "evt_fcde2b2edba56bf408601fb721fe9b5c",
"type": "report.ready",
"created_at": "2026-10-05T09:02:11.000Z",
"org_id": "7f3c1e2a-0b4d-4c8e-9f10-2a3b4c5d6e7f",
"store_id": "0c1d2e3f-1111-4222-8333-444455556666",
"severity": "info",
"data": {
"snapshot_id": "5a6b7c8d-9999-4aaa-8bbb-cccc0000dddd",
"kind": "weekly",
"cadence": "weekly",
"schedule_source": "schedule",
"locale": "pt-BR",
"store_name": "Minha Loja",
"domain": "minhaloja.com.br",
"period": { "start": "2026-09-28T09:00:00.000Z", "end": "2026-10-05T09:00:00.000Z" },
"pdf_url": "https://…/reports/…pdf?token=…",
"url": "https://app.proteside.com/compliance?tab=reports&snapshot=5a6b7c8d-9999-4aaa-8bbb-cccc0000dddd",
"pending_reviews": 3,
"review_due": 1,
"open_alerts": 2,
"open_alerts_by_severity": { "critical": 0, "high": 1, "medium": 1, "low": 0, "info": 0 },
"summary_643": { "...": "..." },
"summary_1161": { "...": "..." },
"digest": { "...": "..." },
"report": {
"id": "5a6b7c8d-9999-4aaa-8bbb-cccc0000dddd",
"kind": "weekly",
"period_start": "2026-09-28T09:00:00.000Z",
"period_end": "2026-10-05T09:00:00.000Z",
"pdf_url": "https://…/reports/…pdf?token=…",
"url": "https://app.proteside.com/compliance?tab=reports&snapshot=5a6b7c8d-9999-4aaa-8bbb-cccc0000dddd"
},
"store": { "name": "Minha Loja", "domain": "minhaloja.com.br" }
}
}O link pdf_url é assinado e vale por 7 dias. Pode vir null se o PDF não estiver disponível.
{
"id": "evt_e4da3b7fbbce2345d7772b0674a318d5",
"type": "test",
"created_at": "2026-10-06T12:11:00.000Z",
"org_id": "7f3c1e2a-0b4d-4c8e-9f10-2a3b4c5d6e7f",
"store_id": null,
"severity": "info",
"data": {
"message": "Test notification from Proteside",
"channel": { "id": "a9b8c7d6-5555-4666-8777-888899990000", "name": "SIEM de produção", "type": "webhook" }
}
}Verificar a assinatura
Quando o canal tem segredo, cada entrega traz:
X-Proteside-Signature: t=1791277201,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bdté o momento do envio em segundos Unix (recalculado a cada tentativa).v1é o HMAC-SHA256, em hexadecimal, det+.+ corpo cru, usando o segredo inteiro como chave, incluindo o prefixowhsec_.
Para verificar:
- Leia o corpo cru, antes de qualquer parse de JSON. Reserializar o JSON muda os bytes e quebra a assinatura.
- Separe
tev1e confira o formato:tinteiro,v1com 64 caracteres hexadecimais. - Recuse entregas com
ta mais de 300 segundos do seu relógio. O Proteside não impõe esse limite: a proteção contra reenvio malicioso é responsabilidade de quem recebe. - Calcule o HMAC esperado e compare em tempo constante.
import express from 'express';
import { createHmac, timingSafeEqual } from 'node:crypto';
const SECRET = process.env.PROTESIDE_WEBHOOK_SECRET; // "whsec_..."
const TOLERANCE_S = 300;
function verifySignature(rawBody, header, secret) {
if (!header) return false;
const parts = {};
for (const item of header.split(',')) {
const i = item.indexOf('=');
if (i > 0) parts[item.slice(0, i).trim()] = item.slice(i + 1).trim();
}
// 1. valida o formato antes de qualquer comparação
if (!/^\d+$/.test(parts.t ?? '') || !/^[0-9a-f]{64}$/i.test(parts.v1 ?? '')) return false;
// 2. janela de tempo contra reenvio
const t = Number(parts.t);
if (Math.abs(Date.now() / 1000 - t) > TOLERANCE_S) return false;
// 3. HMAC-SHA256(segredo, "t." + corpo cru), comparado em tempo constante
const expected = createHmac('sha256', secret).update(`${t}.`).update(rawBody).digest();
const given = Buffer.from(parts.v1, 'hex');
return given.length === expected.length && timingSafeEqual(given, expected);
}
const app = express();
const seen = new Set(); // em produção, use Redis ou o seu banco
// express.raw mantém o corpo como Buffer, sem parse
app.post('/hooks/proteside', express.raw({ type: 'application/json' }), (req, res) => {
if (!verifySignature(req.body, req.get('X-Proteside-Signature'), SECRET)) {
return res.sendStatus(401);
}
const event = JSON.parse(req.body.toString('utf8'));
if (seen.has(event.id)) return res.sendStatus(200); // já processado
seen.add(event.id);
res.sendStatus(204); // responda rápido (menos de 8 s)...
queueMicrotask(() => handleEvent(event)); // ...e processe depois
});
function handleEvent(event) {
console.log(event.type, event.data);
}
app.listen(3000);Trocar o segredo
Envie um novo secret em PATCH /webhooks/{id} (ou edite o Segredo de assinatura no dashboard). A troca vale a
partir da próxima entrega, sem período de convivência: retentativas pendentes também passam a ser assinadas com o
segredo novo. Para não perder eventos, faça o receptor aceitar os dois segredos por alguns minutos, troque no
Proteside e depois remova o antigo.
Entrega e retentativas
- A primeira tentativa sai na hora do evento.
- O seu endpoint tem 8 segundos para responder. Só respostas
2xxcontam como sucesso. - Redirecionamentos não são seguidos: um
301ou302conta como falha. Cadastre a URL final. - Em caso de falha, o Proteside tenta de novo até completar 4 tentativas: a imediata e mais três, cerca de 1 minuto, 10 minutos e 60 minutos depois da falha anterior. As retentativas rodam em ciclos de 5 minutos, então podem atrasar alguns minutos além desses intervalos.
- Depois da quarta falha, a entrega fica como Falhou e não é reenviada. O canal não é desativado sozinho.
- Se o canal estiver desativado na hora da retentativa, a entrega falha sem novo envio.
- Não há garantia de ordem entre eventos.
Pode chegar mais de uma vez
Se o seu endpoint processa o evento mas demora mais de 8 segundos para responder, o Proteside considera falha e
envia de novo. Deduplique pelo id do evento (evt_…), que se mantém nas retentativas, ou pelo header
X-Proteside-Delivery. Responda 2xx rápido e processe em segundo plano.
O histórico das últimas 50 entregas (evento, status, tentativas, código HTTP e erro) fica em Configurações → Notificações, no card Histórico de entregas, por até 90 dias. A API não expõe o histórico nem permite reenviar uma entrega manualmente.
Testar o webhook
Pelo dashboard, clique em Testar conexão no card do canal. Pela API:
curl -s -X POST "https://app.proteside.com/api/v1/webhooks/a9b8c7d6-5555-4666-8777-888899990000/test" \
-H "Authorization: Bearer $PROTESIDE_TOKEN"{ "id": "a9b8c7d6-5555-4666-8777-888899990000", "delivered": false, "error": "http_404: Not Found" }O teste envia um evento test assinado, com uma única tentativa. Ele ignora os filtros de eventos e de severidade e
é enviado mesmo com o canal desativado.
Confira o campo delivered
POST /webhooks/{id}/test responde 200 mesmo quando a entrega falha. Verifique delivered e, se for false, o
motivo em error: http_<status>: <início do corpo>, timeout ou uma mensagem de rede.
Slack, Microsoft Teams e e-mail
Se você só quer avisos para pessoas, não precisa de um endpoint próprio:
- Slack: crie um Incoming Webhook no Slack e cadastre a URL com
"format": "slack"na API, ou o tipo Slack no dashboard. O Proteside envia uma mensagem formatada com o resumo do evento e um botão para abri-lo no dashboard. - Microsoft Teams: o mesmo com
"format": "teams"ou o tipo Teams. A mensagem é um Adaptive Card. - E-mail: só pelo dashboard (tipo E-mail, até 5 destinatários). Cada loja já nasce com o canal "E-mail (padrão)" para o e-mail do proprietário.
As mensagens do Slack e do Teams têm textos fixos em inglês. Os canais de e-mail recebem apenas alert.created,
report.ready e test, mesmo que outros eventos estejam marcados: avisos de integrity.changed, header.changed e
sdk.silent não chegam por e-mail. Para esses, use webhook, Slack ou Teams.