Saltar al contenido

Recibir eventos

Un receptor de webhooks hace siempre las mismas cinco cosas. Aquí las tienes, y un receptor completo en siete lenguajes que puedes copiar y probar en tu equipo.

6 min de lectura

Lo que hace todo receptor

  1. 01
    Lee el cuerpo crudo

    Tal como llega, sin interpretarlo, y hasta 256 KB, que es lo máximo que envía Hablame.

  2. 02
    Revisa la hora

    X-Hablame-Timestamp debe ser un entero a 300 segundos o menos de tu reloj.

  3. 03
    Verifica la firma

    En tiempo constante, con X-Hablame-Signature y, si no coincide, con X-Hablame-Signature-Prev. Cómo se calcula.

  4. 04
    Descarta lo repetido

    Guarda X-Hablame-Delivery-Id con un índice único. Si ya existe, responde 2xx sin procesar otra vez.

  5. 05
    Responde 2xx y procesa aparte

    Responde en cuanto el aviso quede guardado; tienes 15 s en total. El trabajo pesado va después, en una cola.

Receptores de ejemplo

Cada ejemplo es un programa completo que escucha en /webhooks/hablame, lee el secreto de la variable de entorno HABLAME_WEBHOOK_SECRET y el puerto de PORT. Los repetidos se guardan en memoria para que el ejemplo funcione solo; en producción usa tu base de datos. El de PHP ya lo hace con PDO, porque PHP no conserva memoria entre peticiones.

// server.mjs: Node.js 18 o superior, sin dependencias externas.
import { createServer } from 'node:http'
import { createHmac, timingSafeEqual } from 'node:crypto'

// Tu secreto de firma (whsec_...). Léelo de una variable de entorno o de tu gestor de secretos; nunca lo escribas en el código.
const SECRETO = process.env.HABLAME_WEBHOOK_SECRET
const PUERTO = Number(process.env.PORT ?? 3000)
const RUTA = '/webhooks/hablame'
const MAX_CUERPO = 256 * 1024
const TOLERANCIA_SEG = 300

if (!SECRETO) throw new Error('Falta la variable HABLAME_WEBHOOK_SECRET')

// X-Hablame-Delivery-Id no cambia entre reintentos: con él descartas lo que ya recibiste. En producción, guárdalo con un índice único en tu base, junto con el evento.
// Ejemplo en memoria: se pierde al reiniciar.
const recibidos = new Set()

function firmaValida(recibida, esperada) {
  if (typeof recibida !== 'string') return false
  const a = Buffer.from(recibida)
  const b = Buffer.from(esperada)
  return a.length === b.length && timingSafeEqual(a, b)
}

function verificar(cabeceras, cuerpo) {
  // La hora de la firma debe ser un entero a 300 s o menos de tu reloj; así se rechaza un aviso viejo repetido.
  const ts = cabeceras['x-hablame-timestamp']
  if (typeof ts !== 'string' || !/^[0-9]+$/.test(ts)) return false
  if (Math.abs(Math.floor(Date.now() / 1000) - Number(ts)) > TOLERANCIA_SEG) return false

  // Compara en tiempo constante con X-Hablame-Signature y, si no coincide, con X-Hablame-Signature-Prev (llega solo durante las 24 h siguientes a una rotación).
  const esperada = 'sha256=' + createHmac('sha256', SECRETO).update(ts + '.').update(cuerpo).digest('hex')
  return (
    firmaValida(cabeceras['x-hablame-signature'], esperada) ||
    firmaValida(cabeceras['x-hablame-signature-prev'], esperada)
  )
}

function procesar(entrega, cuerpo) {
  // Aquí va tu lógica. En producción, usa una cola durable.
  try {
    const evento = JSON.parse(cuerpo.toString('utf8'))
    console.log('evento procesado', evento.type, evento.id, entrega)
  } catch (error) {
    console.error(error)
  }
}

createServer((req, res) => {
  if (req.method !== 'POST' || req.url !== RUTA) {
    res.writeHead(404).end()
    return
  }

  // Lee el cuerpo CRUDO, sin interpretarlo: la firma se calcula sobre esos bytes exactos. Hablame envía hasta 256 KB.
  const partes = []
  let largo = 0
  req.on('data', (parte) => {
    largo += parte.length
    if (largo <= MAX_CUERPO) partes.push(parte)
  })

  req.on('end', () => {
    if (largo > MAX_CUERPO) {
      res.writeHead(413).end()
      return
    }
    const cuerpo = Buffer.concat(partes)
    if (!verificar(req.headers, cuerpo)) {
      res.writeHead(401).end()
      return
    }

    const entrega = req.headers['x-hablame-delivery-id']
    if (typeof entrega !== 'string' || entrega === '') {
      res.writeHead(400).end()
      return
    }
    if (recibidos.has(entrega)) {
      res.writeHead(200).end()
      return
    }
    recibidos.add(entrega)

    // Responde 2xx de inmediato y procesa aparte. Tienes 15 s en total, y cualquier respuesta distinta de 2xx se reintenta.
    res.writeHead(200).end()
    setImmediate(() => procesar(entrega, cuerpo))
  })
}).listen(PUERTO, () => console.log('Esperando avisos de Hablame en', 'http://localhost:' + PUERTO + RUTA))

Pruébalo en tu equipo

Con el receptor corriendo, este script firma un aviso igual que Hablame y lo envía. Necesitas openssl y curl.

Bash
# 1. Arranca tu receptor en el puerto 3000 con este secreto de ejemplo en su entorno.
export HABLAME_WEBHOOK_SECRET='whsec_TU_SECRETO_DE_FIRMA'

# 2. Firma un aviso como lo hace Hablame y envíalo. La respuesta debe ser 200; si lo envías otra vez, también, pero no se procesa de nuevo.
CUERPO='{"id":"0192f0c47b1e7a3d9c553e2a1b4c5d6e","type":"sms.mo.87654","data":{"shortCode":"87654","from":"573001234567","text":"Hola, quiero más información","receivedAt":"2026-10-24T15:04:05Z","messageId":"0192f0c4-7b1e-7a3d-9c55-3e2a1b4c5d6e"}}'
TS=$(date +%s)
FIRMA="sha256=$(printf '%s' "$TS.$CUERPO" | openssl dgst -sha256 -hmac "$HABLAME_WEBHOOK_SECRET" | sed 's/^.* //')"
curl -i -X POST http://localhost:3000/webhooks/hablame \
  -H 'Content-Type: application/json' \
  -H "X-Hablame-Timestamp: $TS" \
  -H "X-Hablame-Signature: $FIRMA" \
  -H 'X-Hablame-Delivery-Id: wdl_01K8F3Q2Z9X7V4N6B5C2D1E0FA' \
  --data-binary "$CUERPO"

En producción

Haz esto
En vez de esto
Guarda el aviso y su X-Hablame-Delivery-Id en una tabla con índice único, y responde.
Llamar a otros sistemas antes de responder: si tardas más de 15 s, el aviso se reintenta.
Procesa desde una cola durable, que sobreviva a un reinicio.
Procesar en memoria: si tu servidor se reinicia después de responder 2xx, el aviso no vuelve.
Si no pudiste guardar el aviso, responde con un error (por ejemplo 503).
Responder 2xx «para que no insista» sin haberlo guardado.
Registra X-Hablame-Delivery-Id en tus logs: con él soporte encuentra la entrega.
Registrar el secreto o las firmas completas.

¿Necesitas ayuda?

¿Te sirvió esta página?

Ver todos los canales de soporte

Tus preferencias de cookies

Tú decides qué medimos. Puedes cambiar tu elección cuando quieras. Rechazar no te quita ninguna función de la documentación.

Política de privacidad