Regresar
504

Webhooks: patrones y validación de firma

Actualizado: 15/09/2026

Un endpoint que recibe webhooks es la única parte de muchos sistemas que está abierta a internet esperando peticiones de un tercero. No lleva inicio de sesión, no tiene interfaz, y con frecuencia mueve dinero o cambia el estado de una cuenta.

Esa combinación explica por qué merece un artículo propio: es una superficie pequeña, muy expuesta, y con tres o cuatro errores típicos que se repiten en casi todas las integraciones.

Qué es y qué problema resuelve

La alternativa a un webhook es preguntar. Consultar cada minuto si un pago se completó, si un envío cambió de estado, si un documento terminó de procesarse.

El webhook invierte la dirección: en lugar de preguntar tú, el proveedor te avisa. Hace una petición a una dirección que le diste, con los datos del evento, en el momento en que ocurre.

La ganancia es doble: el dato llega al instante y desaparecen las miles de consultas que respondían que no había novedades. Para integraciones con proveedores externos, es casi siempre la opción correcta.

El costo es que ahora tienes un endpoint público que ejecuta lógica de negocio, y que quien lo llama no es tu aplicación.

El camino completo de un webhook

Conviene ver el recorrido entero antes de entrar en detalles, porque casi todos los errores están en algún punto concreto de esta secuencia.

Recorrido de un webhook: el proveedor envía un POST con el cuerpo del evento y una cabecera de firma a tu endpoint, que verifica la firma, encola el trabajo y responde con un 200 en menos de cinco segundos. Si no respondes a tiempo, el proveedor reenvía el mismo evento, que puede llegar varias veces
Responde rápido y haz el trabajo después: el proveedor no espera a que termines.

Las dos reglas que resume el diagrama son las que más incidentes evitan: verificar antes de hacer nada, y responder antes de trabajar.

Verificar la firma, que es lo único que autentica

Tu endpoint no puede saber quién le está llamando. La dirección es pública o descubrible, así que cualquiera puede enviarle un cuerpo con la forma correcta diciendo que un pago de mil euros se completó.

La protección estándar es una firma: el proveedor calcula un resumen del cuerpo usando un secreto compartido, lo envía en una cabecera, y tú repites el cálculo y comparas. Si coincide, el mensaje viene de quien dice venir y no fue alterado.

Hay dos detalles que deciden si la verificación sirve de algo. El primero es que hay que firmar el cuerpo exacto que llegó, en bytes, antes de convertirlo a un objeto. Si se interpreta el JSON y luego se vuelve a serializar, cualquier diferencia de espacios o de orden cambia el resultado y la firma nunca coincide.

El segundo es que la comparación debe ser de tiempo constante. Comparar dos cadenas con el operador normal tarda distinto según cuántos caracteres coincidan al principio, y eso permite deducir la firma correcta a base de intentos. Todos los lenguajes traen una función específica para esto.

// El cuerpo en bruto, sin interpretar
$cuerpo = file_get_contents('php://input');
$firmaRecibida = $_SERVER['HTTP_X_SIGNATURE'] ?? '';

$esperada = hash_hmac('sha256', $cuerpo, $secreto);

// Comparación de tiempo constante: nunca usar ===
if (!hash_equals($esperada, $firmaRecibida)) {
    http_response_code(401);
    exit;
}

// Solo ahora es seguro interpretar el contenido
$evento = json_decode($cuerpo, true);

Para probarlo desde la terminal sin esperar a que el proveedor envíe nada, se puede generar la firma con las herramientas del sistema:

# Calcular la firma de un cuerpo de prueba
CUERPO='{"evento":"pago.completado","id":"8841"}'
SECRETO='mi-secreto-compartido'

FIRMA=$(printf '%s' "$CUERPO" | openssl dgst -sha256 -hmac "$SECRETO" | sed 's/^.* //')
echo "$FIRMA"

# Enviarlo al endpoint local con la cabecera correspondiente
curl -i -X POST http://localhost:8080/webhooks/pagos \
  -H "Content-Type: application/json" \
  -H "X-Signature: $FIRMA" \
  --data "$CUERPO"

Ese par de comandos permite desarrollar la verificación entera sin depender del proveedor, y comprobar que un cuerpo con firma incorrecta se rechaza con 401.

Responder rápido y trabajar después

El segundo error más común es hacer todo el trabajo dentro de la petición del webhook. Generar la factura, enviar el correo, actualizar el inventario, llamar a otro servicio.

Los proveedores esperan una respuesta en pocos segundos, habitualmente cinco o diez. Si tardas más, dan la petición por fallida y la reenvían, aunque tu proceso haya terminado correctamente. El resultado es trabajo duplicado.

El patrón correcto es guardar el evento, responder de inmediato con un código de éxito y procesar después, en una tarea aparte.

// Tras verificar la firma
$pdo->prepare(
    'INSERT INTO eventos_recibidos (id_externo, tipo, cuerpo, procesado)
     VALUES (?, ?, ?, false)
     ON CONFLICT (id_externo) DO NOTHING'
)->execute([$evento['id'], $evento['tipo'], $cuerpo]);

// Responder ya: el trabajo se hace fuera de esta petición
http_response_code(200);
echo 'ok';

Esa cláusula final es importante y hace doble trabajo: guarda el evento y, si ya existía, no hace nada. Es la defensa contra los duplicados, que es el siguiente problema.

El mismo evento va a llegar dos veces

Los proveedores garantizan que el evento llega al menos una vez, no exactamente una vez. Esa diferencia es la fuente del fallo más caro de esta familia.

Llega duplicado cuando tu respuesta tarda, cuando se pierde por el camino, cuando hay un corte de red justo al responder o cuando el proveedor reintenta por precaución. No es una situación excepcional: ocurre con normalidad.

Si el procesamiento no está preparado, un pago se registra dos veces, un correo se envía dos veces y un saldo se suma dos veces. La aplicación funciona perfectamente y los datos quedan mal.

La defensa es guardar el identificador del evento y comprobarlo antes de procesar, que es exactamente lo que hace la restricción de unicidad del ejemplo anterior. El identificador debe ser el del proveedor, no uno generado por ti, porque es el único que se repite entre los reintentos.

El orden también puede llegar cambiado

Menos conocido y igual de real: dos eventos enviados en cierto orden pueden llegar en el contrario, sobre todo si el primero tuvo que reintentarse.

Eso significa que puedes recibir el aviso de que un pedido se envió antes que el de que se pagó. Si el procesamiento asume la secuencia, el estado final queda incoherente.

Las dos formas de protegerse son comprobar la marca de tiempo del evento y descartar los más antiguos que el estado actual, o hacer que cada evento lleve el estado completo en lugar de solo el cambio, de modo que aplicar el último siempre deje el resultado correcto.

Esta segunda opción es la que suele salir mejor y depende del proveedor: conviene mirar en su documentación si el cuerpo trae el objeto completo o solo la diferencia.

Qué hacer cuando algo falla de tu lado

Hay una decisión que confunde a menudo: qué responder cuando el evento llegó bien pero tu procesamiento falla.

La respuesta corta es responder con éxito si el evento se guardó, aunque el trabajo posterior falle. Ya lo tienes; reintentarlo es asunto tuyo, no del proveedor. Devolver un error hace que el proveedor reenvíe, lo que multiplica el problema si la causa del fallo es interna.

Devolver error tiene sentido en un caso: cuando el evento no se pudo guardar, por ejemplo si la base de datos no responde. Ahí sí conviene que el proveedor reintente, porque el evento se perdería.

Y conviene vigilar la cola de eventos sin procesar. Si crece, hay algo roto en el trabajo posterior, y esa es una alerta que no salta sola.

-- Alerta simple: eventos recibidos hace más de 10 minutos sin procesar
SELECT count(*) FROM eventos_recibidos
 WHERE procesado = false
   AND recibido_en < now() - interval '10 minutes';

Desarrollar y depurar sin volverse loco

El endpoint tiene que ser accesible desde internet, lo que complica el desarrollo local. Hay dos caminos y conviene conocer ambos.

El primero es exponer temporalmente el puerto local con un túnel, que es lo que ofrecen varias herramientas de línea de comandos. Permite recibir los eventos reales del proveedor mientras se programa.

El segundo, que suele ser más rápido y no depende de nada externo, es guardar un cuerpo real de ejemplo y reproducirlo con los comandos de más arriba. Una vez tienes la firma calculada, puedes lanzar el mismo evento cuantas veces quieras, incluido el caso del duplicado.

Conviene además registrar cada webhook recibido con su cuerpo completo y su cabecera de firma, al menos durante los primeros días. Es lo que permite reconstruir qué llegó exactamente cuando algo no cuadra, y casi todos los proveedores ofrecen además un historial de envíos en su panel.

La lista de comprobación del endpoint

Resumido en lo que conviene verificar antes de dar por terminada una integración.

  • Rechaza sin firma válida, con comparación de tiempo constante sobre el cuerpo en bruto.
  • Responde en menos de un segundo, guardando y encolando en lugar de procesar.
  • Ignora duplicados por el identificador del proveedor.
  • Tolera el desorden, o comprueba marcas de tiempo.
  • Tiene límite de peticiones, porque es un endpoint público.
  • Limita el tamaño del cuerpo, para que nadie envíe cien megabytes.
  • Solo acepta el método POST y el tipo de contenido esperado.
  • Tiene vigilada la cola de eventos sin procesar.

Son ocho puntos y ninguno lleva más de unos minutos, salvo el segundo si hay que montar el procesamiento aparte.

Qué se le escapa a una IA con esto

Pedir un endpoint para recibir webhooks produce, casi siempre, código que interpreta el cuerpo, ejecuta la lógica de negocio y responde al final. Funciona a la primera con el evento de prueba y le faltan las tres protecciones principales.

Suele faltar la verificación de firma, porque nadie la mencionó. Cuando aparece, es frecuente que compare con el operador normal en lugar de con la función de tiempo constante, y que firme el cuerpo ya interpretado en lugar del original, con lo que la verificación falla siempre o no protege.

Y casi siempre falta el manejo de duplicados, porque el caso solo aparece en producción.

Las frases que cambian la respuesta son pedir que verifique la firma sobre el cuerpo en bruto con comparación de tiempo constante, que responda antes de procesar y que ignore eventos ya recibidos por su identificador. Con esas tres condiciones el código generado sale ya con la estructura correcta.


Guía de referencia intermedio