Webhooks
Proton envía un POST firmado a la dirección de la empresa cuando algo pasa en las automatizaciones: una ejecución
termina, un lote termina o no se ejecuta, una ocurrencia se abre, se asigna o se resuelve, un runner se cae o vuelve.
Es el camino para la herramienta de monitoreo, de tickets o de mensajes de la empresa. Para leer los mismos eventos
bajo demanda, usa GET /v1/events.
Los webhooks forman parte del plan Enterprise. El registro está en Configuraciones > Alertas y webhooks, en la pestaña Webhooks: el nombre, la dirección y los eventos (o todos).
Lo que llega
El envío sigue el estándar Standard Webhooks:
POST https://monitoreo.ejemplo.com/proton
content-type: application/json
user-agent: Proton-Webhooks/1.0
webhook-id: evt_1532
webhook-timestamp: 1760886000
webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4=
{
"type": "run.finished",
"timestamp": "2026-10-19T15:00:00.000Z",
"version": "1",
"data": {
"organizationId": 12,
"link": "https://app.protoncloud.com.br/automations/center/dataset/40/cycle/7/dataset-run/981/details",
"run": {
"id": 981,
"status": "Failed",
"endReason": "technical_error",
"startedAt": "2026-10-19T14:58:00.000Z",
"finishedAt": "2026-10-19T15:00:00.000Z",
"durationMs": 120000,
"correlationId": "7f6c2d2e-5f0b-4a4a-9b9b-3c1f2a9e0d11",
"retryScheduled": false,
"link": "https://app.protoncloud.com.br/automations/center/dataset/40/cycle/7/dataset-run/981/details"
},
"automation": { "id": 5, "name": "Registro de proveedor" },
"dataset": { "id": 40, "name": "Sucursal 01" },
"group": { "id": 3, "name": "Operación", "purpose": "operation" },
"cycle": { "id": 7, "name": "Octubre" },
"environment": { "id": 2, "name": "Producción", "production": true },
"runner": { "id": 9, "name": "rpa-01", "hostname": "rpa-01" }
}
}
Los eventos
| Tipo | Cuándo | Campos en data |
|---|---|---|
run.finished | Una ejecución terminó | run (estado, motivo del fin, inicio, fin, duración, Correlation ID), automation, dataset, group, cycle, environment, runner |
batch.finished | Un lote terminó | batch (total, passed, failed, stopped), group, cycle, environment, runner |
batch.not_run | Una programación no salió | batch (con el reason), group, cycle, environment, runner |
exception.opened, exception.assigned, exception.resolved | Una ocurrencia de la operación se abrió, se asignó o se resolvió | exception (kind, status, occurrences, reason, errorSummary, assignee), automation, dataset, group, cycle, environment |
runner.offline, runner.online | Un runner se cayó (después de 2 minutos fuera) o volvió | runner (name, owner, hostname, offlineSince, shared) |
webhook.test | La prueba desde la pantalla | organizationId, message |
El motivo del fin de la ejecución (endReason) es uno de estos: passed, business_error, technical_error,
environment_error, stopped, not_dispatched, runner_lost.
La versión del cuerpo (version) solo cambia si un campo sale o cambia de sentido. Un campo nuevo puede aparecer en
cualquier momento: trata lo que no conoces como opcional.
Verificar la firma
El secreto del webhook (whsec_...) aparece una sola vez, en el registro y en el cambio. La firma es el HMAC-SHA256 de
webhook-id.webhook-timestamp.cuerpo, con la clave en base64 que viene después de whsec_, codificado en base64 y
precedido de v1,. Verifica siempre el cuerpo exactamente como llegó, antes de interpretar el JSON, y rechaza el
mensaje con un webhook-timestamp muy antiguo (más de 5 minutos).
- Python
- Node.js
import base64
import hashlib
import hmac
import time
def firma_valida(secreto: str, encabezados: dict, cuerpo: bytes, tolerancia: int = 300) -> bool:
clave = base64.b64decode(secreto.removeprefix("whsec_"))
id_del_mensaje = encabezados["webhook-id"]
momento = encabezados["webhook-timestamp"]
if abs(time.time() - int(momento)) > tolerancia:
return False
contenido = f"{id_del_mensaje}.{momento}.".encode() + cuerpo
esperada = base64.b64encode(hmac.new(clave, contenido, hashlib.sha256).digest()).decode()
for firma in encabezados["webhook-signature"].split(" "):
version, _, valor = firma.partition(",")
if version == "v1" and hmac.compare_digest(valor, esperada):
return True
return False
import crypto from 'node:crypto';
export function firmaValida(secreto, encabezados, cuerpo, tolerancia = 300) {
const clave = Buffer.from(secreto.replace(/^whsec_/, ''), 'base64');
const id = encabezados['webhook-id'];
const momento = encabezados['webhook-timestamp'];
if (Math.abs(Date.now() / 1000 - Number(momento)) > tolerancia) return false;
// cuerpo: el texto exactamente como llegó, antes del JSON.parse
const esperada = crypto.createHmac('sha256', clave).update(`${id}.${momento}.${cuerpo}`).digest('base64');
return encabezados['webhook-signature'].split(' ').some((firma) => {
const [version, valor] = firma.split(',');
return version === 'v1'
&& valor.length === esperada.length
&& crypto.timingSafeEqual(Buffer.from(valor), Buffer.from(esperada));
});
}
Las bibliotecas de Standard Webhooks (para Python, Node.js, Java, Go, C# y otras) hacen esta verificación.
Entrega y nuevos intentos
- Respuesta: cualquier
2xxen hasta 10 segundos cuenta como entregado. Responde rápido y procesa después. - Nuevos intentos: sin
2xx, Proton intenta de nuevo con espera creciente (30 segundos, 1 minuto, 2 minutos... hasta 1 hora entre uno y otro), hasta 8 intentos. - Repetición: el
webhook-ides el mismo en todos los intentos del mismo evento. Guarda los ids recibidos y descarta el repetido. - Dirección: solo
https, en una dirección pública. Las redirecciones no se siguen. - Seguimiento: la pestaña Historial de Alertas y webhooks muestra cada entrega, con el resultado, el estado
HTTP y el error, y permite reenviar la que falló. El botón de prueba envía un
webhook.test.