Webhooks
Recibe alertas, scripts nuevos y reportes listos en tu endpoint, verifica la firma HMAC y gestiona los reintentos.
Con un webhook, Proteside envía un POST a tu endpoint en cuanto algo ocurre: una alerta nueva, un script desconocido
en el checkout, un reporte PCI DSS listo. No necesitas consultar la API constantemente.
Webhooks, Slack, Microsoft Teams y correo electrónico son todos canales de notificación de la misma organización. Lo que creas mediante la API aparece en Ajustes → Notificaciones, y viceversa.
Crear un webhook
Abre Ajustes → Notificaciones y haz clic en Agregar canal. Debes ser propietario o administrador.
Elige el tipo Webhook y el alcance: Esta tienda (solo eventos de la tienda seleccionada) u Organización (eventos de todas las tiendas). El tipo y el alcance no se pueden cambiar después.
Completa Nombre y URL (debe empezar con https://) y deja Formato en JSON (firmado).
Completa Secreto de firma con un valor aleatorio de 16 a 128 caracteres y guárdalo: lo vas a necesitar para verificar las entregas.
Elige la Severidad mínima y los Eventos, mantén Canal activo encendido y haz clic en Guardar cambios.
Sin secreto, sin firma
Desde el dashboard el secreto es opcional. Si el campo queda vacío, las entregas salen sin el header
X-Proteside-Signature y la tarjeta del canal muestra "sin firma". Completa siempre el secreto en los webhooks JSON.
Eventos
| Evento | Cuándo se envía | severity | Filtrado por min_severity |
|---|---|---|---|
alert.created | Se abre una alerta nueva (primera ocurrencia), incluidas las violaciones de CSP | la de la alerta | sí |
alert.resolved | Una alerta de SDK silencioso se resuelve sola porque el SDK volvió a enviar eventos | medium | sí |
integrity.changed | El contenido de un script autorizado cambió (alerta SCRIPT_INTEGRITY_MISMATCH) | high | sí |
header.changed | Un header de seguridad de la página de pago cambió (alerta HEADER_CHANGED) | medium | sí |
sdk.silent | Una tienda con tráfico pasó 24 horas sin enviar eventos del SDK (alerta SDK_SILENT) | medium | sí |
script.detected | Apareció un script nuevo en el checkout y espera revisión | info | no |
policy.applied | Una política de aprobación decidió sobre un script (modo automático o Aplicar) | info | no |
report.ready | El reporte PCI DSS programado está listo (lunes, 09:00 UTC) | info | no |
test | Disparaste una prueba | info | ignora todos los filtros |
Eventos del catálogo que hoy no se envían
script.authorized y script.blocked aparecen en la lista de eventos del dashboard y se aceptan en el campo
events, pero Proteside no los envía hoy. Para seguir las autorizaciones y los bloqueos, consulta GET /scripts o
la auditoría.
alert.resolved solo se envía en la resolución automática de SDK silencioso. Resolver una alerta desde el dashboard
o con PATCH /alerts/{id} no genera ningún evento. Los reportes generados con GET /reports/pci tampoco generan
report.ready: solo el envío programado lo genera.
Filtros del canal
Cada canal tiene dos filtros:
events: lista de eventos aceptados. Vacía onullsignifica todos.min_severity: aplica solo a los eventos de alerta (alert.created,alert.resolved,integrity.changed,header.changedysdk.silent).script.detected,policy.appliedyreport.readypasan siempre.
El valor predeterminado high descarta los eventos medium
Los canales nuevos nacen con min_severity: high (Alto o superior). Con ese valor, sdk.silent,
header.changed y las alertas medium o low (como las violaciones de CSP) nunca llegan. Si quieres saber
cuándo el SDK dejó de funcionar, usa min_severity: "medium" (Medio o superior) o menor.
Formato de la entrega
Proteside hace un POST con cuerpo JSON y estos headers:
| Header | Contenido |
|---|---|
Content-Type | application/json |
User-Agent | Proteside-Webhooks/1.0 |
X-Proteside-Event | Tipo de evento, por ejemplo alert.created |
X-Proteside-Delivery | ID de la entrega (único por canal y evento, igual en todos los reintentos) |
X-Proteside-Signature | t=<unix>,v1=<hex> (presente cuando el canal tiene secreto) |
Todos los eventos usan el mismo sobre:
Propiedad
Tipo
Ejemplos 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": "Mi Tienda",
"domain": "mitienda.com"
}
}
}alert.resolved, integrity.changed, header.changed y sdk.silent usan el mismo 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": "Mi Tienda", "domain": "mitienda.com" }
}
}{
"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": "mitienda.com",
"host": "mitienda.com",
"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": "Mi Tienda", "domain": "mitienda.com" }
}
}{
"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": "Mi Tienda", "domain": "mitienda.com" }
}
}{
"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 del catálogo", "version": 1 },
"action": "authorize",
"justification": "Tag manager de marketing, sin acceso a campos de pago.",
"expires_at": "2027-01-04T09:13:00.000Z",
"actor": "api:ps_live_Ab3dEf6hIj9kLm2nOp5qRs8t",
"store": { "id": "0c1d2e3f-1111-4222-8333-444455556666", "name": "Mi Tienda", "domain": "mitienda.com" }
}
}{
"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": "Mi Tienda",
"domain": "mitienda.com",
"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": "Mi Tienda", "domain": "mitienda.com" }
}
}El enlace pdf_url está firmado y vale durante 7 días. Puede venir en null si el PDF no está disponible.
{
"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 producción", "type": "webhook" }
}
}Verificar la firma
Cuando el canal tiene secreto, cada entrega incluye:
X-Proteside-Signature: t=1791277201,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bdtes el momento del envío en segundos Unix (se recalcula en cada intento).v1es el HMAC-SHA256, en hexadecimal, det+.+ cuerpo crudo, usando el secreto completo como clave, incluido el prefijowhsec_.
Para verificarla:
- Lee el cuerpo crudo, antes de cualquier parseo de JSON. Volver a serializar el JSON cambia los bytes y rompe la firma.
- Separa
tyv1y revisa el formato:tentero,v1con 64 caracteres hexadecimales. - Rechaza las entregas con un
ta más de 300 segundos de tu reloj. Proteside no impone ese límite: la protección contra reenvíos maliciosos es responsabilidad de quien recibe. - Calcula el HMAC esperado y compáralo en tiempo 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 el formato antes de cualquier comparación
if (!/^\d+$/.test(parts.t ?? '') || !/^[0-9a-f]{64}$/i.test(parts.v1 ?? '')) return false;
// 2. ventana de tiempo contra reenvíos
const t = Number(parts.t);
if (Math.abs(Date.now() / 1000 - t) > TOLERANCE_S) return false;
// 3. HMAC-SHA256(secreto, "t." + cuerpo crudo), comparado en tiempo 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(); // en producción, usa Redis o tu base de datos
// express.raw mantiene el cuerpo como Buffer, sin parsearlo
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); // ya procesado
seen.add(event.id);
res.sendStatus(204); // responde rápido (menos de 8 s)...
queueMicrotask(() => handleEvent(event)); // ...y procesa después
});
function handleEvent(event) {
console.log(event.type, event.data);
}
app.listen(3000);Cambiar el secreto
Envía un nuevo secret en PATCH /webhooks/{id} (o edita el Secreto de firma en el dashboard). El cambio aplica
desde la siguiente entrega, sin período de convivencia: los reintentos pendientes también pasan a firmarse con el
secreto nuevo. Para no perder eventos, haz que el receptor acepte los dos secretos durante algunos minutos, cámbialo en
Proteside y después quita el anterior.
Entrega y reintentos
- El primer intento sale en el momento del evento.
- Tu endpoint tiene 8 segundos para responder. Solo las respuestas
2xxcuentan como éxito. - Las redirecciones no se siguen: un
301o302cuenta como falla. Registra la URL final. - En caso de falla, Proteside lo vuelve a intentar hasta completar 4 intentos: el inmediato y tres más, unos 1 minuto, 10 minutos y 60 minutos después de la falla anterior. Los reintentos se ejecutan en ciclos de 5 minutos, así que pueden retrasarse algunos minutos más allá de esos intervalos.
- Después de la cuarta falla, la entrega queda como Falló y no se reenvía. El canal no se desactiva solo.
- Si el canal está desactivado en el momento del reintento, la entrega falla sin un nuevo envío.
- No hay garantía de orden entre eventos.
Puede llegar más de una vez
Si tu endpoint procesa el evento pero tarda más de 8 segundos en responder, Proteside lo considera una falla y lo
vuelve a enviar. Deduplica por el id del evento (evt_…), que se mantiene en los reintentos, o por el header
X-Proteside-Delivery. Responde 2xx rápido y procesa en segundo plano.
El historial de las últimas 50 entregas (evento, estado, intentos, código HTTP y error) está en Ajustes → Notificaciones, en la tarjeta Historial de entregas, durante hasta 90 días. La API no expone el historial ni permite reenviar una entrega manualmente.
Probar el webhook
Desde el dashboard, haz clic en Probar conexión en la tarjeta del canal. Mediante la 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" }La prueba envía un evento test firmado, con un único intento. Ignora los filtros de eventos y de severidad y se envía
aunque el canal esté desactivado.
Revisa el campo delivered
POST /webhooks/{id}/test responde 200 incluso cuando la entrega falla. Verifica delivered y, si es false, el
motivo en error: http_<status>: <inicio del cuerpo>, timeout o un mensaje de red.
Slack, Microsoft Teams y correo electrónico
Si solo quieres avisos para personas, no necesitas un endpoint propio:
- Slack: crea un Incoming Webhook en Slack y registra la URL con
"format": "slack"en la API, o el tipo Slack en el dashboard. Proteside envía un mensaje con formato, con el resumen del evento y un botón para abrirlo en el dashboard. - Microsoft Teams: lo mismo con
"format": "teams"o el tipo Teams. El mensaje es una Adaptive Card. - Correo electrónico: solo desde el dashboard (tipo Correo, hasta 5 destinatarios). Cada tienda nace con el canal "E-mail (padrão)" para el correo del propietario.
Los mensajes de Slack y Teams tienen textos fijos en inglés. Los canales de correo electrónico reciben solo
alert.created, report.ready y test, aunque haya otros eventos marcados: los avisos de integrity.changed,
header.changed y sdk.silent no llegan por correo. Para esos, usa webhook, Slack o Teams.