Ir al contenido

Webhooks

En vez de preguntar si un job terminó, deja que te avisemos. Enviamos un evento con POST a tu URL cada vez que un job cambia:

Evento Cuándo
job.processing El job salió de la cola y empezó.
job.succeeded Todos los resultados están listos.
job.failed Falló: data.error dice por qué.
job.canceled Se canceló.

Los eventos van a cada endpoint de tu organización (se añaden en el panel, en Webhooks) y al webhook_url propio del job, si lo tiene.

POST /hooks/iomza HTTP/1.1
Content-Type: application/json
User-Agent: Iomza-Webhooks/1.0
webhook-id: 0199a0e3-9c1a-7f02-b6d8-3e7a4c0f5b91
webhook-timestamp: 1793462400
webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4=
{
"id": "0199a0e3-9c1a-7f02-b6d8-3e7a4c0f5b90",
"object": "event",
"type": "job.succeeded",
"created_at": "2026-10-30T18:04:11.000Z",
"data": { "id": "0199a0e1-…", "object": "job", "status": "succeeded", "outputs": { "…": "…" } }
}

data es el job, tal como lo muestra GET /v1/jobs/{id} (sin billed_minutes).

Responde con cualquier 2xx en menos de 10 segundos y haz el trabajo después. Cualquier otra cosa (otro código, una redirección, un tiempo agotado) cuenta como intento fallido, y el evento se envía de nuevo tras 1 min, 5 min, 30 min, 2 h, 6 h, 12 h y 24 h. Puedes ver cada envío y su resultado, y reenviarlo, en el panel, o listarlos:

GET /v1/webhook-deliveries?job_id={id del job}

Los webhooks siguen Standard Webhooks. Cada petición va firmada con el secreto de tu organización (whsec_…, en el panel, en Webhooks), para que compruebes que viene de nosotros y no se alteró:

  1. Toma las cabeceras webhook-id y webhook-timestamp y el cuerpo crudo, exactamente como llegó (antes de interpretar el JSON).
  2. Calcula HMAC-SHA256 de <webhook-id>.<webhook-timestamp>.<cuerpo>, con la parte del secreto tras whsec_ decodificada de base64 como clave.
  3. Compárala, en base64, con la firma tras v1, en webhook-signature, en tiempo constante.
  4. Rechaza marcas de tiempo a más de 5 minutos de tu reloj, para que una petición capturada no se pueda repetir después.

Las librerías oficiales standardwebhooks lo hacen todo:

import { Webhook } from "standardwebhooks";
const wh = new Webhook(process.env.IOMZA_WEBHOOK_SECRET); // "whsec_…"
app.post("/hooks/iomza", express.raw({ type: "application/json" }), (req, res) => {
let event;
try {
event = wh.verify(req.body, req.headers); // lanza un error si la firma o la hora no cuadran
} catch {
return res.sendStatus(400);
}
res.sendStatus(204);
handle(event); // después de responder
});

O a mano, en Node:

import { createHmac, timingSafeEqual } from "node:crypto";
function verify(secret, headers, rawBody) {
const id = headers["webhook-id"];
const timestamp = Number(headers["webhook-timestamp"]);
if (Math.abs(Date.now() / 1000 - timestamp) > 300) return false;
const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
const expected = createHmac("sha256", key).update(`${id}.${timestamp}.${rawBody}`).digest();
return headers["webhook-signature"].split(" ").some((s) => {
const given = Buffer.from(s.replace(/^v1,/, ""), "base64");
return given.length === expected.length && timingSafeEqual(given, expected);
});
}

Rotar el secreto en el panel lo reemplaza al instante: los envíos desde entonces se firman con el nuevo.

La entrega es al menos una vez: un fallo de red después de que respondiste puede hacer que enviemos un evento otra vez. Guarda los id de los eventos que ya procesaste y salta los repetidos. Los eventos también pueden llegar desordenados (un job.processing reintentado después de job.succeeded, por ejemplo): fíate del status del job en data, o consulta el job, no del orden de llegada.

  • https, en una dirección pública: las direcciones privadas e internas se rechazan, y se comprueba de nuevo en cada envío.
  • Responde al propio POST: las redirecciones no se siguen.