Webhooks

Notificaciones por webhook

Deja de consultar en bucle. Enviamos el código SMS a tu servidor en cuanto llega, firmado y con reintentos.

Por qué webhooks

Consultar en bucle gasta peticiones y añade retraso. Un webhook llega a tu servidor en el mismo segundo en que aparece el código.

  • Sin bucle de consulta: tu cuota diaria de peticiones se dedica al trabajo real y no a comprobar estados.
  • Menos latencia: el código llega a tu sistema en cuanto lo recibimos, no en tu siguiente consulta.
  • También los eventos finales: las cancelaciones y las expiraciones llegan con el estado del reembolso, así tu contabilidad se mantiene al día.

Configuración

Registra una URL por cuenta con tu clave de API. El secreto de firma se devuelve una sola vez.

curl -X POST https://smsbulk.net/api/users/webhook \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://your-app.com/smsbulk/webhook"}'

Respuesta

{
  "id": "wep_2f8c1a9d",
  "url": "https://your-app.com/smsbulk/webhook",
  "sourceFilter": "API",
  "secret": "whsec_5f3a9c1e8b2d47a6903c5e1f7d8b4a26"
}

Guarda el secreto ahora

El secreto en claro solo aparece en esta respuesta. Las lecturas posteriores devuelven un valor enmascarado. Si lo pierdes, renuévalo desde el panel o con el endpoint de rotación.

Requisitos de la URL

  • HTTPS en el puerto 443. Se rechazan HTTP simple y otros puertos.
  • Una dirección pública. Los rangos privados, loopback y las direcciones de metadatos de nube se rechazan al registrar y de nuevo en cada entrega.
  • Sin redirecciones. Una respuesta 3xx no se sigue y cuenta como intento fallido.
MétodoRutaPropósito
POST/api/users/webhookRegistra o reemplaza el endpoint y devuelve el secreto una vez
GET/api/users/webhookLee el endpoint actual, el secreto va enmascarado
PATCH/api/users/webhookCambia la URL o el filtro de canal, o reactiva un endpoint desactivado
POST/api/users/webhook/rotateRenueva el secreto de firma con una ventana de transición
POST/api/users/webhook/testEnvía un evento de prueba sin datos de activación
DELETE/api/users/webhookElimina el endpoint y detiene todas las notificaciones
GET/api/users/webhook/deliveriesÚltimos 20 intentos de entrega, solo metadatos

Eventos

Tres eventos, todos sobre una misma activación.

EventoCuándo se envía
activation.code_receivedCuando llega el código SMS. Es el evento sobre el que actúan casi todas las integraciones.
activation.cancelledCuando la activación se cancela. El cuerpo indica el motivo y si se devolvió el saldo.
activation.expiredCuando la activación agota su tiempo sin código. El cuerpo indica el estado del reembolso.

Filtro de canal

Por defecto solo los pedidos hechos por API generan notificación. Pon sourceFilter en null si también quieres los pedidos que haces desde el panel.

Petición y cuerpo

Cada entrega es un POST con cuerpo JSON. Los campos se eligen uno a uno: el enrutamiento interno y los detalles de coste nunca aparecen.

Forma de la petición

POST /smsbulk/webhook HTTP/1.1
Content-Type: application/json
User-Agent: SMSBulk-Webhook/1
X-SMSBulk-Signature: t=1785312000,v1=8f2a...c41d
X-SMSBulk-Event-Id: evt_del_9f2c1a
X-SMSBulk-Event-Type: activation.code_received
X-SMSBulk-Delivery-Attempt: 1

activation.code_received

{
  "id": "evt_del_9f2c1a",
  "type": "activation.code_received",
  "created": 1785312000,
  "data": {
    "activation_id": "cms2u6x80d1twmkudp2cfbqo4",
    "status": "RECEIVED",
    "phone_number": "447700900123",
    "service": "wa",
    "country": "GB",
    "code": "483920",
    "price": "0.42",
    "currency": "USD",
    "source": "API",
    "created_at": "2026-08-04T09:12:00.000Z",
    "sms_text": "Your code is 483920",
    "received_at": "2026-08-04T09:12:34.000Z"
  }
}

activation.cancelled y activation.expired

{
  "id": "evt_del_7b31de",
  "type": "activation.cancelled",
  "created": 1785312600,
  "data": {
    "activation_id": "cms2u6x80d1twmkudp2cfbqo4",
    "status": "CANCELLED",
    "phone_number": "447700900123",
    "service": "wa",
    "country": "GB",
    "code": null,
    "price": "0.42",
    "currency": "USD",
    "source": "API",
    "created_at": "2026-08-04T09:12:00.000Z",
    "reason": "user_cancel",
    "refunded": true,
    "refund_amount": "0.42"
  }
}

En versiones futuras pueden aparecer campos nuevos. Ignora los campos desconocidos en lugar de fallar con ellos.

Verificar la firma

Cada petición lleva una firma HMAC SHA256 construida con la marca de tiempo y el cuerpo en bruto.

X-SMSBulk-Signature: t=<unix_seconds>,v1=<hex_hmac_sha256>
signed_string = "<t>.<raw_request_body>"

Verifica los bytes en bruto

Calcula la firma sobre el cuerpo exacto que recibiste. Si analizas el JSON y lo vuelves a serializar, cambian el orden de las claves y los espacios, y la firma no coincidirá.

Puede haber más de un v1

Durante una rotación de secreto la cabecera lleva dos valores v1. Recógelos en una lista, no en un diccionario: un diccionario solo guarda el último y el secreto nuevo quedaría sin verificar en silencio.

Node.js

const crypto = require('crypto');

function verify(rawBody, header, secret) {
  const pairs = header.split(',').map((p) => {
    const i = p.indexOf('=');
    return [p.slice(0, i), p.slice(i + 1)];
  });

  const t = pairs.find(([k]) => k === 't')?.[1];
  // During a rotate window more than one signature arrives: collect them all.
  const signatures = pairs.filter(([k]) => k === 'v1').map(([, v]) => v);
  if (!t || signatures.length === 0) return false;

  // Replay window: reject anything older than 5 minutes.
  if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;

  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${t}.${rawBody}`)
    .digest('hex');

  return signatures.some(
    (s) =>
      s.length === expected.length &&
      crypto.timingSafeEqual(
        Buffer.from(s, 'hex'),
        Buffer.from(expected, 'hex'),
      ),
  );
}

Python

import hmac, hashlib, time

def verify(raw_body: bytes, header: str, secret: str) -> bool:
    pairs = [p.split("=", 1) for p in header.split(",")]

    t = next((v for k, v in pairs if k == "t"), None)
    # During a rotate window more than one signature arrives: collect them all.
    signatures = [v for k, v in pairs if k == "v1"]
    if t is None or not signatures:
        return False

    # Replay window: reject anything older than 5 minutes.
    if abs(time.time() - int(t)) > 300:
        return False

    expected = hmac.new(
        secret.encode(),
        f"{t}.".encode() + raw_body,
        hashlib.sha256,
    ).hexdigest()

    return any(hmac.compare_digest(s, expected) for s in signatures)

La marca de tiempo forma parte de la cadena firmada, así que puedes rechazar lo que tenga más de unos minutos. Compara las firmas en tiempo constante.

Respuesta y reintentos

Responde rápido. Esperamos 10 segundos por la respuesta.

Haz

  • Devuelve 2xx en cuanto hayas guardado el evento.
  • Encola el trabajo pesado y responde primero.
  • Trata un 4xx propio como un error: detiene los reintentos.

No hagas

  • No ejecutes tareas largas antes de responder, la petición expira a los 10 segundos.
  • No respondas con 3xx, las redirecciones no se siguen.
  • No construyas un cuerpo de respuesta grande, se descarta sin leerlo.
IntentoEspera previaactivation.code_receivedcancelled y expired
1inmediato
210s
360s
45m
515m
630m

Se reintenta ante errores de red, tiempos de espera, 408, 429 y 5xx. Cualquier otro 4xx detiene la escalera de inmediato. code_received se corta en 4 intentos porque la activación termina mucho antes de que los pasos siguientes sirvan de algo.

Deduplicación

Usa X-SMSBulk-Event-Id como clave de idempotencia. Los reintentos del mismo evento llevan el mismo id.

Al menos una vez, no exactamente una vez

Se crea un registro por activación y tipo de evento, así que los duplicados son raros. Aun así, la recuperación de un trabajo atascado puede entregar el mismo registro otra vez. Guarda el id del evento e ignora un id que ya hayas procesado.

Cortacircuitos

Un endpoint muerto no se llama para siempre.

  • Tras 20 entregas fallidas seguidas el endpoint se desactiva.
  • Recibes un correo y el panel muestra por qué se desactivó.
  • Los eventos creados mientras el endpoint estaba desactivado no se reenvían. Ponte al día con GET /v1/activations.

Rotar el secreto

Cambia el secreto de firma sin cortes.

  • El endpoint de rotación devuelve el nuevo secreto una vez.
  • Durante la ventana de transición se envían la firma nueva y la anterior.
  • Pasa al secreto nuevo a tu ritmo, tras la ventana solo se envía la firma nueva.

Configura tu endpoint

Registra una URL, envía un evento de prueba y sigue la lista de entregas en tu panel.