Webhooks

Cuando algo interesante pasa con tus servicios, Hablame te lo notifica con un POST a una URL que tú controlas. Esta página documenta el contrato: el sobre del evento, los headers de firma, cómo verificarlos y qué hacer ante reintentos.

7 min de lectura

Cómo se da de alta un endpoint

La gestión de endpoints (alta, baja, rotación del secreto e historial de entregas) se hace desde el portal de clientes, no desde la API pública: el sistema solo te envía webhooks, no necesitas llamar nada para configurarlos. Hay dos modalidades.

Endpoint permanente

Se da de alta una vez desde el portal, eligiendo el servicio y los tipos de evento que quieres escuchar (o todos, por defecto). Cada endpoint trae su propio secreto, con el que se firman todas sus entregas.

URL por petición

Algunos endpoints de la API aceptan un campo con una URL puntual, por ejemplo webhookUrl en el lote de Number Insight. Esa URL recibe el evento firmado con el secreto de cuenta, compartido por todas las URLs por petición.

Contrato HTTP

Cada entrega es un POST con cuerpo JSON. Devuelve cualquier 2xx en menos de unos pocos segundos para confirmar la recepción; cualquier otra cosa (4xx, 5xx o timeout) se considera fallo y entra en reintentos.

POST a tu endpoint
POST /webhooks/hablame HTTP/1.1
Host: example.com
Content-Type: application/json
User-Agent: Hablame-Webhooks/1
X-Hablame-Event-Type:  sms.delivered
X-Hablame-Delivery-Id: b85b3d50-7c44-4ed8-aac6-3c2b6a4fe1aa
X-Hablame-Timestamp:   1781832862
X-Hablame-Signature:   sha256=8c4f3c2b7a91...
Idempotency-Key:       b85b3d50-7c44-4ed8-aac6-3c2b6a4fe1aa

{ "id": "...", "type": "sms.delivered", "version": 1 }

El sobre del evento

Todos los eventos tienen la misma forma, sin importar el servicio. Un servicio nuevo agrega type nuevos sin cambiar la estructura.

CampoTipoSignificado
iduuidIdentificador del evento lógico.
typestringTipo con puntos, por ejemplo sms.delivered, voice.answered o numberinsight.batch.completed.
versionnumberVersión del sobre. Hoy 1; un cambio incompatible sube este número.
servicestringServicio emisor (sms, email, voice, numberinsight...).
accountIdnumberCuenta a la que pertenece el evento.
occurredAtISO-8601Cuándo ocurrió el evento, con zona horaria.
dataobjectContenido específico del type.

Headers de cada entrega

X-Hablame-Event-Type
El type del evento: útil para enrutar antes de interpretar el JSON.
X-Hablame-Delivery-Id
Identificador único de esta entrega. Cambia entre reintentos del mismo evento.
X-Hablame-Timestamp
Momento de la firma, en segundos epoch. Se usa para detectar reenvíos.
X-Hablame-Signature
Firma HMAC del cuerpo, en formato sha256=<hex>.
Idempotency-Key
Igual al identificador de entrega. Sirve para que tu lado deduplique entre reintentos del mismo evento.
Content-Type
Siempre application/json.

Verificar la firma HMAC

Calculamos la firma con HMAC-SHA256 sobre la cadena "{timestamp}.{cuerpo_crudo}", usando el secreto del endpoint (o el de cuenta para URLs por petición). El header trae sha256=<hex>. Para verificar:

  1. Lee el cuerpo crudo, sin volver a serializar el JSON: un cambio de espacios invalida la firma.
  2. Lee X-Hablame-Timestamp y descarta si está fuera de tu ventana de tolerancia. 5 minutos es razonable.
  3. Calcula hex(HMAC-SHA256(secreto, timestamp + "." + cuerpo)).
  4. Compara con la parte después de sha256= usando una comparación de tiempo constante: hash_equals en PHP, hmac.compare_digest en Python.
  5. Si hay una rotación en curso, acepta también el secreto anterior durante la ventana de rotación.
<?php
function verifyHablameSignature(string $rawBody, array $headers, string $secret): bool
{
    $sig = $headers['x-hablame-signature'] ?? '';
    $ts  = (int) ($headers['x-hablame-timestamp'] ?? 0);

    // Anti-reenvio: 5 minutos.
    if (abs(time() - $ts) > 300) {
        return false;
    }
    if (!str_starts_with($sig, 'sha256=')) {
        return false;
    }

    $expected = hash_hmac('sha256', $ts . '.' . $rawBody, $secret);

    return hash_equals($expected, substr($sig, 7));
}

Idempotencia: qué debes asumir

La entrega es al menos una vez: ante una caída intermedia (la red, tu servidor o el nuestro) reenviamos el mismo evento con el mismo identificador de entrega. Cualquier integración seria debe deduplicar por ese header: guárdalo en una tabla con restricción de unicidad y descarta la segunda inserción.

Si tu manejador hace algo que no es idempotente (un cargo, un mensaje en un chat), la deduplicación tiene que envolver eso. No basta con contestar 200 y procesar después: si tu servicio se cae entre el 200 y la confirmación, pierdes el evento.

Reintentos y retroceso

Si una entrega falla, la reencolamos con retroceso exponencial y variación aleatoria (aproximadamente 5 s, 10 s, 20 s, con techo de 1 hora) hasta unos 12 intentos. Después de eso la entrega queda marcada como fallida y entra a una cola de revisión que monitoreamos. Los reintentos conservan la prioridad: los eventos urgentes van por una vía dedicada para entrega casi inmediata.

Tras varios fallos consecutivos sobre el mismo endpoint lo deshabilitamos de forma automática, para no seguir enviando a una URL muerta. Una entrega exitosa posterior lo vuelve a habilitar.

Seguridad

  • Verifica la firma siempre. Sin verificación, cualquiera puede hacer POST a tu URL haciéndose pasar por Hablame.
  • Valida el timestamp. Una firma vieja reenviada sigue siendo válida, pero su timestamp es antiguo. Una ventana de ±5 minutos elimina el reuso.
  • Toma el secreto del entorno, no del repositorio. Rótalo periódicamente desde el portal. Durante la rotación firmamos con el secreto nuevo y el anterior; tu verificador puede probar ambos.
  • HTTPS obligatorio. Validamos la URL antes de entregar: bloqueamos direcciones de bucle local y rangos privados, y solo aceptamos http y https reales.
  • Responde rápido. Si tu manejador tarda, encola internamente y devuelve 2xx de inmediato: si no, la entrega cae por timeout y entra a reintentos.