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.
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.
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 /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.
| Campo | Tipo | Significado |
|---|---|---|
id | uuid | Identificador del evento lógico. |
type | string | Tipo con puntos, por ejemplo sms.delivered, voice.answered o numberinsight.batch.completed. |
version | number | Versión del sobre. Hoy 1; un cambio incompatible sube este número. |
service | string | Servicio emisor (sms, email, voice, numberinsight...). |
accountId | number | Cuenta a la que pertenece el evento. |
occurredAt | ISO-8601 | Cuándo ocurrió el evento, con zona horaria. |
data | object | Contenido específico del type. |
Headers de cada entrega
X-Hablame-Event-Type- El
typedel 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:
- Lee el cuerpo crudo, sin volver a serializar el JSON: un cambio de espacios invalida la firma.
- Lee
X-Hablame-Timestampy descarta si está fuera de tu ventana de tolerancia. 5 minutos es razonable. - Calcula
hex(HMAC-SHA256(secreto, timestamp + "." + cuerpo)). - Compara con la parte después de
sha256=usando una comparación de tiempo constante:hash_equalsen PHP,hmac.compare_digesten Python. - 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
timestampes 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
httpyhttpsreales. - 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.