Seguimiento

Webhooks de entrega con firmas

Configura un endpoint HTTPS en la página de Webhooks de la consola. Cuando un mensaje alcanza un estado final, enviamos un POST con un evento JSON a ese endpoint.

Eventos

typeCuando
message.deliveredEl mensaje fue entregado.
message.failedEl mensaje falló; data.error tiene el código. Su precio ha sido reembolsado.
POST to your endpointJSON
{
  "id": "evt_8F3K2Q9TZ1MXD",
  "type": "message.delivered",
  "created_at": "2026-11-07T09:30:17.902Z",
  "data": {
    "id": "msg_8F3K2Q9TZ1MX",
    "object": "message",
    "batch_id": "bat_4M7Q1X0PK2RS",
    "to": "+14155550123",
    "country": "US",
    "text": "Your Northwind code is 482913. It expires in 10 minutes.",
    "segments": 1,
    "price": "0.00507",
    "currency": "USD",
    "status": "delivered",
    "error": null,
    "reference": "login-7781",
    "created_at": "2026-11-07T09:30:12.418Z",
    "sent_at": "2026-11-07T09:30:13.020Z",
    "done_at": "2026-11-07T09:30:17.902Z"
  }
}

Encabezados y firma

Cada llamada lleva tres encabezados, en el formato Standard Webhooks:

EncabezadoValor
webhook-idEl ID del evento (evt_...). El mismo ID se utiliza para cada reintento de un evento.
webhook-timestampTiempo Unix en segundos cuando se firmó este intento.
webhook-signaturev1, seguido de la firma HMAC-SHA256 en base64. Pueden aparecer varias firmas, separadas por espacios: acepta la llamada cuando alguna coincida.

Para verificar una llamada: toma el secreto de la consola (comienza con whsec_), decodifica en base64url la parte después de whsec_ para obtener la clave, calcula HMAC-SHA256 de webhook-id + "." + webhook-timestamp + "." + raw body, codifícalo en base64 y compáralo con la firma en tiempo constante. Rechaza marcas de tiempo que estén a más de cinco minutos de tu reloj. Siempre usa el cuerpo crudo: un cuerpo JSON analizado y re-serializado no coincidirá.

// Express: keep the raw body, check the signature, then answer 2xx quickly
import crypto from "node:crypto";
import express from "express";

const app = express();
const secret = process.env.BULKSMSAPI_WEBHOOK_SECRET; // whsec_...

app.post("/hooks/sms", express.raw({ type: "application/json" }), (req, res) => {
  const id = req.header("webhook-id");
  const ts = req.header("webhook-timestamp");
  const key = Buffer.from(secret.slice("whsec_".length), "base64url");
  const expected = crypto.createHmac("sha256", key).update(`${id}.${ts}.${req.body}`).digest("base64");
  const sent = (req.header("webhook-signature") || "").split(" ").map((s) => s.replace(/^v1,/, ""));
  const fresh = Math.abs(Date.now() / 1000 - Number(ts)) < 300;
  if (!fresh || !sent.some((s) => s.length === expected.length && crypto.timingSafeEqual(Buffer.from(s), Buffer.from(expected)))) {
    return res.status(401).end();
  }
  const event = JSON.parse(req.body);
  console.log(event.type, event.data.id, event.data.status);
  res.status(204).end();
});
app.listen(3000);

Respuesta y reintentos

  • Responde con cualquier estado 2xx dentro de 10 segundos. Haz el trabajo lento después de responder, desde una cola.
  • No se siguen redirecciones: apunta la configuración a la dirección final.
  • Una llamada que se agota el tiempo o responde con algo distinto a 2xx se intenta nuevamente después de 30 segundos, 2 minutos, 10 minutos, 30 minutos, 1 hora, 2 horas, 4 horas, 8 horas, 8 horas: 10 intentos en total, aproximadamente 24 horas desde el primero.
  • Cada intento se enumera en la página de Webhooks con el estado HTTP y el tiempo que tomó.

Duplicados y orden

Un evento puede llegar dos veces, por ejemplo, cuando tu endpoint lo guardó pero la respuesta se perdió. Almacena webhook-id y omite los IDs que ya hayas manejado. Los eventos para diferentes mensajes pueden llegar en cualquier orden; lee data.status y data.done_at en lugar de confiar en el orden de llegada.

Reglas del endpoint

  • Solo HTTPS, en un nombre de host público (sin literales de IP, localhost o redes privadas).
  • El secreto puede ser reemplazado en la página de Webhooks en cualquier momento; las llamadas firmadas después de eso usan el nuevo secreto.
  • Eliminar el endpoint detiene nuevos eventos; los mensajes que ya terminaron mantienen su estado para ser consultados.
Webhooks de entrega con firmas | Documentación de BulkSMSAPI