Ir al contenido

Verificar webhooks firmados de Payora en PHP

Guía para validar la firma de webhooks de Payora en PHP usando el payload bruto, encabezados y HMAC de forma segura.

Payora11 min de lecturaEN · RU · UK · ES · DE · FR · IT · PL · PT · HI · ZH
Verificar webhooks firmados de Payora en PHP

Qué son los webhooks firmados de Payora

Los webhooks firmados de Payora son solicitudes HTTP que llevan una firma digital, para que tu endpoint en PHP pueda comprobar que el evento realmente proviene de Payora y no fue alterado en el camino. Si estás aprendiendo cómo validar la firma de un webhook de Payora en PHP, el flujo básico es sencillo: Payora envía un evento, tu servidor lee el payload en bruto y tu código verifica la firma antes de confiar en cualquier dato del cuerpo.

Esa comprobación importa porque un webhook puede llegar desde cualquier cliente que conozca tu URL. Un atacante puede copiar el endpoint de los registros y luego hacer un POST con un evento de pago falso y un cuerpo JSON aparentemente legítimo. La firma evita ese truco.

El evento en sí solo es útil después de la verificación. Antes de eso, no es más que texto.

Piénsalo como un boleto numerado en la entrada. El cuerpo es el mensaje, la firma es el sello y tu código en PHP decide si ambas partes coinciden. Si coinciden, procesas el evento. Si no, lo rechazas de inmediato.

Si ya gestionas un flujo de pagos en el sitio, la misma disciplina aparece en otras partes de la pila. Quien lea una guía sobre gateway de pago cripto para ecommerce verá el mismo patrón: aceptar menos, verificar más y solo entonces actualizar el pedido.

Requisitos previos para un endpoint de webhook en PHP

No necesitas un framework enorme para esto. Un endpoint en PHP 8.x es suficiente, y PHP 7.4 puede funcionar para algunos proyectos, pero el código debe tener acceso al cuerpo bruto de la solicitud y a los encabezados que envía Payora. Eso significa que tu servidor debe permitir el acceso normal a las solicitudes HTTP, no una capa personalizada que reescriba o elimine encabezados.

Usa HTTPS. Sin TLS, el webhook viaja a la vista de todos, y una firma por sí sola no soluciona una seguridad de transporte débil. Un certificado, una configuración correcta del virtual host y una URL pública accesible son la base.

Dos extensiones de PHP importan en la práctica: hash para trabajar con HMAC y, si vas a manejar JSON, las funciones JSON nativas que vienen con PHP. También puede venirte bien cURL para hacer pruebas desde tu lado, pero el endpoint del webhook no lo necesita.

También necesitas el secreto o clave de firma de Payora que coincida con la configuración del webhook en tu cuenta de Payora. Guarda ese valor fuera de la raíz web. No lo pongas en git. Un secreto en un repositorio público es una forma rápida de convertir la verificación en puro teatro.

Una cosa más: tu endpoint debería aceptar solo solicitudes POST. Las solicitudes GET pueden devolver 405, y esa pequeña restricción ahorra tiempo durante la depuración porque acota el tráfico que realmente inspeccionas.

Captura el payload bruto del webhook y los encabezados

Las comprobaciones de firma dependen de los bytes exactos que envió Payora. En PHP, lee el cuerpo bruto de la solicitud con file_get_contents('php://input'). No reconstruyas el payload a partir de $_POST ni de JSON decodificado, porque cualquiera de esas opciones puede cambiar espacios, el orden de las claves o la codificación.

La misma regla aplica a los encabezados. Necesitas el encabezado de firma. Usa getallheaders() donde esté disponible, o lee variables de servidor como $_SERVER['HTTP_X_...'] si tu entorno elimina la ayuda para encabezados. Las pequeñas diferencias de hosting importan aquí.

Imagina un cuerpo de webhook de 412 bytes. Si tu código lo decodifica y lo vuelve a codificar, quizá sigas viendo los mismos campos, pero los bytes ya no serán idénticos. Esa única discrepancia basta para romper la verificación.

Quédate con este hábito: captura primero, analiza después. Primero guarda la cadena cruda. Luego revisa los encabezados. Solo después toca la estructura del payload.

Si pruebas flujos de pago antes del lanzamiento, el mismo hábito ahorra tiempo en otros puntos. El artículo sobre cómo probar un pago cripto aplica la misma lógica a eventos de sandbox, donde la fidelidad del payload es lo que hace que la prueba sea útil.

Reconstruye la firma esperada en PHP

El patrón habitual es un HMAC calculado a partir del payload bruto y un secreto compartido. Empieza con el algoritmo que indique la documentación de webhooks de Payora. Si la documentación dice SHA-256, usa SHA-256. Si menciona un prefijo, una etiqueta de versión o un formato de marca de tiempo, incluye exactamente esa estructura.

Una forma común es esta: construir el mensaje firmado a partir de la marca de tiempo y el cuerpo bruto, y luego ejecutar hash_hmac() con el secreto compartido y el algoritmo hash indicado. El resultado puede necesitar codificación hexadecimal o base64, según la especificación. No adivines el formato. Léelo en la configuración del webhook.

Aquí tienes un ejemplo webhook Payora PHP mínimo, con marcadores donde el formato de Payora puede diferir:

$rawBody = file_get_contents('php://input');
$timestamp = $_SERVER['HTTP_X_PAYORA_TIMESTAMP'] ?? '';
$signature = $_SERVER['HTTP_X_PAYORA_SIGNATURE'] ?? '';
$secret = getenv('PAYORA_WEBHOOK_SECRET');

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

Ese ejemplo solo muestra una forma, no una garantía. Si Payora firma solo el payload, quita la marca de tiempo de la cadena. Si Payora firma una forma JSON canónica, sigue exactamente ese método, no el que prefieras. Un delimitador incorrecto hará fallar todas las solicitudes.

Para los comerciantes que también se preocupan por reglas de facturación y casos límite, la publicación sobre límites de facturas en pagos cripto es un buen complemento, porque el manejo de webhooks y la política de facturación suelen coincidir en el mismo ciclo de vida del pedido.

Compara la firma calculada con la firma de Payora

Una vez que tengas la firma esperada, compárala con la firma entrante usando una comparación en tiempo constante. En PHP, eso suele significar hash_equals(). Las comprobaciones simples con == o === no son la herramienta adecuada aquí, porque pueden filtrar pistas de tiempo y además son menos defensivas ante casos sutiles de discrepancia.

Antes de comparar, normaliza el formato exactamente como Payora espera. Si el encabezado está en hex, compara en hex. Si el encabezado está en base64, compara en base64. Si la firma entrante tiene un prefijo como sha256=, quítalo o inclúyelo exactamente como indiquen los documentos.

Flujo de ejemplo:

  1. Lee el cuerpo bruto.
  2. Lee el encabezado de firma.
  3. Construye la firma esperada a partir de la fórmula documentada.
  4. Compara ambas cadenas con hash_equals().
  5. Rechaza la solicitud con 401 o 400 si no coinciden.

Un 401 tiene sentido cuando la firma es incorrecta. Un 400 tiene sentido cuando faltan encabezados obligatorios. En cualquier caso, la respuesta debe ser breve. Sin pistas extra.

Una frase basta aquí: no imprimas el secreto. Ese error es poco común y es catastrófico.

Maneja la marca de tiempo, la protección contra reenvíos y las reglas de tolerancia

Si Payora incluye una marca de tiempo o un nonce, compruébalo antes de aceptar el evento. Esto bloquea ataques de repetición, donde un atacante reenvía más tarde un webhook válido e intenta hacer que tu aplicación pague dos veces o envíe dos veces. La ventana de tiempo debe seguir la regla de Payora, no tu suposición.

El patrón común es comparar la marca de tiempo del webhook con la hora de tu servidor y rechazar solicitudes obsoletas fuera de la ventana de tolerancia. Si la documentación dice 5 minutos, usa 5 minutos. Si dice 300 segundos, guarda 300 segundos como configuración con nombre y evita codificar un número misterioso en tres archivos.

La deriva del reloj puede romper tráfico válido. Un servidor desfasado 7 minutos puede rechazar una solicitud legítima. Sincroniza tus servidores con NTP y mantén la zona horaria configurada de forma coherente. La zona horaria no cambia las marcas Unix, pero una mala configuración local aún puede hacer que los registros sean más difíciles de leer.

Los nonces son aún más simples cuando existen. Guarda cada nonce una sola vez, márcalo como usado y rechaza un duplicado. Una pequeña caché o una tabla de base de datos puede encargarse de ello. Un nonce, una aceptación.

Si además aceptas precios fijos en las páginas de pago, aquí es donde entra la política. Otra publicación sobre si debo usar precio fijo puede ayudarte a alinear las reglas de tipo de cambio con el momento del webhook para no confirmar el importe incorrecto después de un evento tardío.

Procesa los eventos válidos y devuelve la respuesta HTTP correcta

Después de pasar la verificación, analiza el payload y decide según el tipo de evento. Un evento payment.succeeded no debería seguir el mismo flujo que un event refund.updated. Sepáralos con claridad. Así, cada controlador hace una sola tarea y tus registros siguen siendo legibles.

La lógica de negocio debe ejecutarse antes de enviar la respuesta 2xx. Si marcas un pedido como pagado, escribe primero la fila en la base de datos. Si encolas un correo, encola primero. Si cualquiera de esos pasos falla, devuelve un 500 y deja que Payora lo reintente más tarde.

Este orden importa porque una respuesta 2xx normalmente le dice a Payora que el evento fue aceptado. Si devuelves 200 demasiado pronto y luego fallas al escribir en la base de datos, puedes crear una discrepancia entre Payora y tu aplicación. Ese tipo de error sale caro a las 2 de la mañana.

Un cuerpo de respuesta pequeño es suficiente. “OK” funciona. Mejor aún, no devuelvas ningún cuerpo y mantén claro el código de estado. No devuelvas el payload de vuelta.

Para los equipos que también facturan a clientes a partir de eventos de pago, el artículo sobre gateway de pago cripto para freelancers muestra cómo un webhook puede pasar de un evento técnico a una acción real de facturación sin añadir pasos innecesarios.

Solución de problemas de verificaciones de firma fallidas

La mayoría de los fallos provienen de cinco sitios: el cuerpo bruto cambió, el nombre del encabezado es incorrecto, el secreto es incorrecto, la codificación difiere o la comprobación de la marca de tiempo está fuera de la tolerancia. Empieza por el cuerpo. Si registraste el payload bruto y la firma calculada, compáralos antes de tocar la base de datos.

La mutación del cuerpo es común. El middleware del framework puede recortar espacios en blanco, decodificar JSON o normalizar los finales de línea. Un salto de línea extra basta. Si Payora firmó {"a":1} y tu aplicación lee {"a":1}\n, el HMAC no coincidirá.

El siguiente problema es la discrepancia de encabezados. Algunos servidores convierten guiones en guiones bajos. Algunas capas proxy eliminan encabezados personalizados a menos que se permitan explícitamente. Si tu encabezado de firma no aparece en PHP, revisa la configuración del servidor web y del proxy, no solo el código.

Los problemas de codificación son traicioneros. UTF-8 y Unicode escapado pueden verse bien en los registros y aun así producir bytes diferentes. Si tu aplicación reescribe JSON, la firma puede romperse aunque el payload parezca idéntico en un navegador.

Un secreto incorrecto es el fallo más simple y el que la gente revisa al final. Verifica por separado la variable de entorno, el secreto de staging y el secreto de producción. Un secreto copiado de la cuenta equivocada puede desperdiciar toda una tarde.

La deriva del reloj crea otro síntoma: la firma coincide, pero la solicitud sigue siendo rechazada porque la marca de tiempo es demasiado antigua. Si eso pasa, revisa NTP, los relojes de los contenedores y cualquier proxy inverso que marque su propia hora. Una pequeña deriva de infraestructura puede parecer una firma mala.

Si tu instalación también se integra con WordPress o con facturación de membresías, el artículo sobre cómo aceptar pagos en USDT es útil porque muestra cómo el manejo de webhooks, el control de acceso y la confirmación de pago encajan en un mismo flujo.

Una última prueba ayuda mucho: envía el mismo payload dos veces. La primera solicitud debe pasar. La segunda debe fallar si almacenas y rechazas un nonce reutilizado, o solo debe pasar si el diseño de tu webhook no usa defensa contra repetición basada en nonce. Ese duplicado te dice si tu protección realmente funciona.

Compartir

Comentarios

¿Listo para empezar?

Crea una cuenta y ten tu primera factura funcionando en menos de una hora.