Los webhooks fuera de orden en PHP son un problema de consistencia, no sólo de conectividad. Un proveedor puede reenviar una entrega porque no recibió una respuesta válida, una cola puede retrasar un mensaje o dos eventos de una misma entidad pueden viajar por rutas distintas. Si la aplicación supone que cada evento llega una vez y en secuencia, una confirmación antigua puede sobrescribir una anulación posterior, o una repetición puede ejecutar dos veces una operación irreversible.
La regla de partida es sencilla: un webhook es una notificación de que algo pudo haber cambiado en otro sistema. No es, por sí mismo, una instrucción fiable para mutar el estado local sin comprobaciones. El diseño debe conservar la evidencia recibida, decidir qué eventos son admisibles y aplicar cambios de forma idempotente y ordenada según las reglas del dominio.
Separar recepción, validación y aplicación al dominio

El endpoint HTTP debe hacer poco y hacerlo de forma predecible. Su responsabilidad es recibir la solicitud, verificarla, persistir un registro inmutable y responder dentro del plazo esperado por el emisor. El trabajo que modifica pedidos, suscripciones, inventario o cualquier otra entidad de negocio debería ocurrir después, normalmente mediante un proceso asíncrono.
Separar fases evita que una caída transitoria de una API interna convierta una entrega válida en un reintento ambiguo. También permite reanudar el procesamiento sin pedir al proveedor que reenvíe eventos antiguos.
- Recepción: capturar cabeceras, cuerpo sin transformar, instante de recepción y origen identificado.
- Validación de entrada: comprobar firma, formato, tamaño, tipo de contenido y campos mínimos.
- Persistencia: almacenar el evento y su estado inicial en una transacción corta.
- Encolado: señalar que existe trabajo pendiente, sin depender de procesarlo dentro de la respuesta HTTP.
- Aplicación: un worker interpreta el evento, obtiene el estado necesario y ejecuta una transición de negocio controlada.
Es importante distinguir una entrega de un evento. Una misma entrega puede repetirse, y algunos proveedores asignan un identificador distinto a cada intento de entrega. Si existe un identificador de evento estable, ése suele ser la mejor base para la deduplicación. Si no existe, habrá que definir una clave con origen, entidad externa, tipo y una versión o marca temporal con significado conocido.
Qué registrar para poder auditar y reprocesar
Una tabla de eventos no debe guardar sólo el JSON interpretado. Conserve el cuerpo original, porque normalizarlo antes de almacenarlo puede eliminar información necesaria para verificar una firma, investigar un incidente o adaptar un parser posterior.
Como mínimo, el registro debería contener:
- Origen o proveedor y entorno de integración.
- Identificador externo del evento y, si existe, identificador de entrega.
- Tipo de evento, identificador de entidad externa y versión, secuencia o fecha efectiva.
- Cabeceras relevantes y carga original protegida frente a modificaciones.
- Instante de recepción local y, separadamente, la marca temporal declarada por el emisor.
- Huella criptográfica de la carga para diagnóstico y deduplicación auxiliar.
- Estado de procesamiento: recibido, validado, pendiente, aplicado, ignorado, fallido o en revisión.
- Número de intentos, error resumido, instante del último intento y referencia a la entidad local afectada.
Una restricción única sobre (origen, external_event_id) resuelve la repetición cuando el proveedor ofrece un ID estable. Inserte primero y trate el conflicto como una entrega ya conocida, no como un error de negocio. La respuesta puede seguir siendo satisfactoria para frenar reintentos.
Pero deduplicar el mensaje no basta para garantizar idempotencia. Por ejemplo, dos eventos distintos pueden expresar la misma confirmación y ambos intentar crear un movimiento contable. La operación de negocio debe tener su propia protección: una clave de idempotencia, una restricción única sobre el efecto o una transición que compruebe si el resultado ya existe.
Validar autenticidad y limitar la superficie de entrada
No acepte un webhook porque viene de una dirección IP esperada o porque incluye un campo que parece secreto. Cuando el proveedor lo permita, valide una firma calculada sobre el cuerpo bruto y una marca temporal. La comparación debe ser de tiempo constante y la ventana temporal debe limitar reproducciones, teniendo en cuenta el desfase razonable de relojes.
Antes de persistir, imponga límites operativos: tamaño máximo del cuerpo, tiempo de lectura, formatos aceptados y esquema mínimo. Un JSON válido no es necesariamente un evento válido. Rechace tipos desconocidos si no existe una política explícita para archivarlos sin aplicar efectos.
Los secretos de firma requieren rotación. Durante un cambio, puede ser necesario aceptar una clave anterior y otra nueva durante un periodo delimitado, registrando cuál validó la entrega. No incluya cuerpos completos, tokens ni datos personales innecesarios en logs de aplicación. El registro de auditoría debe tener controles de acceso y una política de retención acorde con la sensibilidad de los datos.
Decidir el orden lógico, no confiar en el orden de red
La hora de recepción no define qué ocurrió primero. Tampoco una fecha incluida en el payload es siempre suficiente: puede ser aproximada, pertenecer a la creación del evento y no a la transición, o verse afectada por relojes no sincronizados. La mejor señal es una versión monotónica o un número de secuencia por entidad proporcionado por el sistema origen.
Cuando exista una versión, guarde la última versión aplicada en la entidad local. Un worker puede aplicar un evento sólo si su versión es superior a la almacenada; una versión igual indica repetición, y una inferior es un evento tardío. Si hay huecos de secuencia, no invente el estado intermedio: marque la entidad para conciliación o consulte la API fuente, si esa API es el registro de referencia.
Si no hay secuencia ni versión, las reglas deben ser de dominio. Una máquina de estados explícita es más segura que asignar directamente un texto recibido. Por ejemplo, una entidad anulada podría impedir volver a confirmada salvo una transición documentada y autorizada. El modelo debe definir qué hacer con cada combinación de estado actual y evento entrante.
if ($eventVersion <= $entity->lastExternalVersion) {
markIgnored($event, 'version_no_mas_reciente');
return;
}
applyAllowedTransition($entity, $event);
$entity->lastExternalVersion = $eventVersion;El código ilustra el criterio, no sustituye la transacción ni las reglas de transición. Para eventos sin versión, una comparación de fechas sólo es aceptable si el contrato del emisor garantiza su semántica y precisión.
Tratar eventos tardíos según el coste de equivocarse
No todos los eventos retrasados merecen la misma respuesta. Elegir entre ignorar, registrar, recalcular o compensar depende de si el evento puede cambiar una obligación real y de cuál es la fuente de verdad.
- Ignorar: apropiado para una versión antigua cuyo efecto ya está incluido en un estado posterior verificable.
- Registrar y alertar: útil si la secuencia es incoherente o falta información para decidir sin intervención.
- Recalcular: consultar el estado actual en el sistema externo y actualizar el espejo local cuando la fuente externa prevalece.
- Compensar: crear una acción correctiva trazable cuando un efecto anterior ya produjo consecuencias y no puede borrarse de forma segura.
Considere un caso hipotético de una operación externa. Llega una confirmación con versión 12, después una anulación con versión 13 y, más tarde, se reintenta la confirmación 12. Con control de versión, la repetición no revive la operación. Si la anulación llega primero y el sistema conoce que falta la versión 12, puede aplicar la anulación si la máquina de estados lo permite o pedir una conciliación antes de producir un efecto sensible.
Concurrencia interna, colas y bloqueos por entidad
El procesamiento asíncrono mejora la capacidad de respuesta, pero introduce carreras internas: dos workers pueden leer el mismo estado antes de que uno escriba. La deduplicación del evento no evita esta condición.
Para entidades sensibles, serialice por clave de entidad externa o local. Esto puede lograrse con particiones de cola basadas en esa clave, un bloqueo distribuido con vencimiento cuidadosamente diseñado o un bloqueo de fila dentro de una transacción breve. Otra opción es el control optimista: actualizar sólo si la versión almacenada sigue siendo la esperada y reintentar al detectar conflicto.
Evite mantener una transacción abierta mientras llama a servicios remotos. Primero reserve o lea el estado de forma consistente; después realice la llamada con una clave idempotente cuando sea posible; finalmente registre el resultado. Si el proceso falla entre pasos, un reintento debe poder distinguir una operación pendiente de una ya completada.
Operación, observabilidad y pruebas antes de publicar

Un panel operativo debe mostrar cuántos eventos permanecen pendientes, fallan repetidamente, se ignoran por antigüedad, se rechazan por firma y presentan huecos de secuencia. Mida también la antigüedad de la cola y el tiempo desde recepción hasta aplicación. Estas señales permiten detectar una integración degradada antes de que el desfase se convierta en un problema de negocio.
Conserve mecanismos de reproceso que partan del evento original y de una versión explícita del parser o manejador. Reprocesar no significa ejecutar ciegamente: limite el alcance, registre quién lo solicitó y mantenga activas las mismas garantías de idempotencia.
Lista de comprobación
- Enviar el mismo evento varias veces, incluso de forma concurrente.
- Entregar una anulación antes de la confirmación relacionada.
- Retrasar un evento antiguo hasta después de uno con versión superior.
- Introducir huecos, tipos desconocidos, cargas truncadas y firmas inválidas.
- Simular la caída del worker después de crear un efecto externo y antes de marcar el evento aplicado.
- Verificar que dos workers sobre la misma entidad no producen una transición imposible.
- Comprobar que el reproceso conserva auditoría y no duplica efectos.
La integración robusta no intenta forzar que la red entregue en orden. Diseña una frontera fiable: guarda cada entrada verificable, aplica reglas de negocio idempotentes, usa un orden lógico cuando exista y concilia cuando no puede conocer el estado con seguridad.



