Blog

Integraciones 9 min de lectura

Integración WayForPay en PHP: formulario de pago, firma y devolución de llamada

WayForPay es la forma práctica de aceptar pagos con tarjeta en UAH, USD y EUR de una empresa ucraniana. Esta guía recorre la integración completa en PHP: firmar el formulario de pago, recibir la devolución de llamada del servidor, responderla correctamente y confirmar el estado del pedido.

Imagen de portada: Integración WayForPay en PHP: formulario de pago, firma y devolución de llamada

En resumen

  • El flujo tiene tres partes: un formulario de pago firmado que envía al cliente a WayForPay, una devolución de llamada de servidor a servidor (serviceUrl) y su propia página de estado del pedido.
  • Cada solicitud y respuesta se firma con HMAC_MD5 utilizando su clave secreta comercial; El orden de los campos importa.
  • Marque un pedido como pagado únicamente a partir de una devolución de llamada verificada o una respuesta CHECK_STATUS, nunca del cliente que regresa a su sitio.
  • Debes responder a la devolución de llamada con una respuesta JSON firmada de "aceptación"; de lo contrario, WayForPay seguirá intentándolo.
  • Mantenga los montos, la moneda y la referencia del pedido en su base de datos y compárelos con lo que dice la devolución de llamada.

Una integración WayForPay en PHP es una pequeña cantidad de código, pero tiene varios lugares donde un error silencioso significa que los pedidos no pagados se marcan como pagados o los pedidos pagados que nunca se cumplen. Realizamos el proceso de pago WayForPay en nuestros propios sitios, incluidos los servicios pagos en este sitio web, y lo hemos conectado a varias tiendas de clientes. A continuación se muestra la estructura que utilizamos, con los detalles que enumera la documentación pero que la gente tiende a hojear.

Cómo funciona el flujo de pago

  1. El cliente hace clic en "Pagar". Su servidor crea un pedido en la base de datos con una orderReference única, una cantidad fija y una moneda.
  2. Su servidor crea los campos del formulario de pago, los firma y genera un formulario HTML de envío automático que se publica en la página de pago WayForPay.
  3. El cliente paga en la página WayForPay (tarjeta, Apple Pay, Google Pay y otros métodos habilitados en su cuenta de comerciante).
  4. WayForPay llama a su serviceUrl con el resultado de la transacción. Esta llamada de servidor a servidor es en lo que usted confía.
  5. El cliente es redirigido a su returnUrl, donde muestra el estado actual del pedido.

El paso 5 puede ocurrir antes de que finalice el paso 4, por lo que la página de devolución debe decir "confirmando su pago" hasta que se haya procesado la devolución de llamada.

Lo que necesitas de la cuenta mercantil

  • merchantAccount: su inicio de sesión de comerciante.
  • merchantSecretKey: el secreto utilizado para las firmas. Guárdelo en una variable de entorno o en un archivo de configuración sin seguimiento, nunca en git o en el código de front-end.
  • merchantDomainName: el dominio de su sitio, tal como está registrado en la cuenta.
  • Las monedas y métodos de pago habilitados para su cuenta.

Paso 1. Cree y firme el formulario de pago

La firma es un HMAC con MD5 sobre campos seleccionados unidos por un punto y coma. Para una compra, el pedido es: cuenta de comerciante, nombre de dominio del comerciante, referencia del pedido, fecha del pedido (marca de tiempo Unix), monto, moneda, luego todos los nombres de los productos, todos los recuentos de productos y todos los precios de los productos.

function wfpSignature(array $parts, string $secret): string
{
    return hash_hmac('md5', implode(';', $parts), $secret);
}

$orderReference = 'ORD-' . $orderId . '-' . time();
$orderDate      = time();
$amount         = '50.00';          // string with a dot, the same value everywhere
$currency       = 'EUR';
$products       = [['name' => 'Consultation, 1 hour', 'count' => 1, 'price' => '50.00']];

$parts = [$merchantAccount, $merchantDomain, $orderReference, $orderDate, $amount, $currency];
foreach ($products as $p) { $parts[] = $p['name']; }
foreach ($products as $p) { $parts[] = (string) $p['count']; }
foreach ($products as $p) { $parts[] = $p['price']; }

$signature = wfpSignature($parts, $secretKey);

Luego renderice el formulario. Los campos de producto son matrices, por lo que utilizan el sufijo []:

<form id="wfp" method="post" action="https://secure.wayforpay.com/pay" accept-charset="utf-8">
  <input type="hidden" name="merchantAccount" value="...">
  <input type="hidden" name="merchantAuthType" value="SimpleSignature">
  <input type="hidden" name="merchantDomainName" value="example.com">
  <input type="hidden" name="merchantSignature" value="<?= htmlspecialchars($signature) ?>">
  <input type="hidden" name="orderReference" value="<?= htmlspecialchars($orderReference) ?>">
  <input type="hidden" name="orderDate" value="<?= $orderDate ?>">
  <input type="hidden" name="amount" value="50.00">
  <input type="hidden" name="currency" value="EUR">
  <input type="hidden" name="productName[]" value="Consultation, 1 hour">
  <input type="hidden" name="productCount[]" value="1">
  <input type="hidden" name="productPrice[]" value="50.00">
  <input type="hidden" name="returnUrl" value="https://example.com/pay/return">
  <input type="hidden" name="serviceUrl" value="https://example.com/pay/callback">
</form>
<script>document.getElementById('wfp').submit();</script>

Los errores de firma más comunes son: un formato de número diferente en la firma y el formulario (50 vs 50.00), un nombre de producto que se cambia (recortado, con escape HTML) después de firmar y olvidar que el orden de los campos es fijo.

Paso 2. Reciba la devolución de llamada en serviceUrl

Cuando el pago alcanza un estado final, WayForPay publica el resultado en serviceUrl. Lea el cuerpo sin formato (es JSON; algunas configuraciones también entregan campos de formulario, así que maneje ambos) y verifique la firma. Para la devolución de llamada, se calcula sobre: cuenta de comerciante, referencia del pedido, monto, moneda, código de autorización, máscara de tarjeta, estado de la transacción y código de motivo.

$raw  = file_get_contents('php://input');
$data = json_decode($raw, true) ?: $_POST;

$line = implode(';', [
    $data['merchantAccount'] ?? '',
    $data['orderReference'] ?? '',
    $data['amount'] ?? '',
    $data['currency'] ?? '',
    $data['authCode'] ?? '',
    $data['cardPan'] ?? '',
    $data['transactionStatus'] ?? '',
    $data['reasonCode'] ?? '',
]);
$expected = hash_hmac('md5', $line, $secretKey);

if (!hash_equals(strtolower($expected), strtolower((string) ($data['merchantSignature'] ?? '')))) {
    http_response_code(400);
    exit;
}

Utilice hash_equals para la comparación para evitar fugas de tiempo. Sólo después de que la firma sea válida debe buscar el pedido y comparar amount y currency con lo que almacenó. Si difieren, regístrelo y no marque el pedido como pagado.

Paso 3. Actualiza el pedido y responde correctamente

El campo transactionStatus le indica lo que sucedió: Approved significa que el dinero está capturado; Valores Declined, Expired y similares significan fracaso; Pending o InProcessing significa esperar; Refunded y Voided significa que el dinero fue devuelto. Actualice su pedido en consecuencia, en una transacción, y hágalo idempotente: si el pedido ya está pagado, no haga nada y aun así responda correctamente.

WayForPay requiere una respuesta firmada para saber que recibió el mensaje. La firma está sobre la referencia del pedido, la palabra accept y la hora actual:

$time   = time();
$status = 'accept';
$reply  = [
    'orderReference' => $data['orderReference'],
    'status'         => $status,
    'time'           => $time,
    'signature'      => hash_hmac('md5', "{$data['orderReference']};{$status};{$time}", $secretKey),
];

header('Content-Type: application/json');
echo json_encode($reply);

Si su servidor no responde así, WayForPay considera la entrega como fallida y lo vuelve a intentar, lo que significa que recibirá la misma devolución de llamada varias veces. Ésa es otra razón para el control de idempotencia.

Paso 4. Confirma el estado con CHECK_STATUS

Es posible que se pierdan devoluciones de llamada: su servidor estaba inactivo, un firewall bloqueó la solicitud, una implementación interrumpió la ruta. WayForPay ofrece una llamada API CHECK_STATUS que devuelve el estado actual de un pedido por su referencia. Úselo en dos lugares: en la página de devolución, si la devolución de llamada no ha llegado después de unos segundos, y en un pequeño trabajo cron que vuelve a verificar los pedidos estancados en "pendientes" durante más de, digamos, quince minutos. La solicitud está firmada con HMAC_MD5 a través de la cuenta del comerciante y la referencia del pedido.

$payload = [
    'transactionType'   => 'CHECK_STATUS',
    'merchantAccount'   => $merchantAccount,
    'orderReference'    => $orderReference,
    'merchantSignature' => hash_hmac('md5', "{$merchantAccount};{$orderReference}", $secretKey),
    'apiVersion'        => 1,
];

$ch = curl_init('https://api.wayforpay.com/api');
curl_setopt_array($ch, [
    CURLOPT_POST           => true,
    CURLOPT_POSTFIELDS     => json_encode($payload),
    CURLOPT_HTTPHEADER     => ['Content-Type: application/json'],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 15,
]);
$result = json_decode((string) curl_exec($ch), true);

Aplique las mismas comprobaciones que para una devolución de llamada: verifique la firma de la respuesta cuando esté presente, compare el monto y la moneda y actualice el pedido de manera idempotente.

Pruebas antes de publicar

  • Utilice el modo de prueba y las tarjetas de prueba de la documentación WayForPay hasta que se hayan visto en sus registros un pago completo, un rechazo y un reembolso.
  • Se debe poder acceder a su serviceUrl desde Internet a través de HTTPS. En localhost, utilice un túnel o implemente en un dominio provisional.
  • Registre cada devolución de llamada con su cuerpo sin formato, el resultado de la verificación y el resultado. Nunca registre la clave secreta.
  • Asegúrese de que el WAF o la protección contra bots (por ejemplo, en Cloudflare) no desafíe ni bloquee las solicitudes de devolución de llamada de WayForPay.

Errores comunes

  • Confiar en la URL de retorno. El cliente puede abrirlo sin pagar.
  • Reutilizar una referencia de pedido. Cada intento necesita una referencia única o confundirás los intentos.
  • Cambiar el importe después de firmar, o mostrar un precio y firmar otro.
  • No responder a la devolución de llamada con la respuesta de aceptación firmada.
  • Marcar como pagado antes de verificar el monto y la moneda.
  • Varias monedas, un secreto compartido y sin asignación: mantenga la moneda en el orden y verifíquela al devolver la llamada.

¿Cómo podemos ayudar?

Creamos integraciones de pagos de extremo a extremo: WayForPay, Stripe, PayPal, facturación recurrente, reembolsos e informes de conciliación. Si ya tiene una caja en funcionamiento pero no está seguro de sus modos de falla, reserve un análisis de arquitectura y revisaremos el manejo de devolución de llamadas, la idempotencia y el registro. Para obtener una guía similar sobre otro proveedor, consulte Webhooks Stripe en PHP y, para conocer el enfoque general, lea nuestro manual de integraciones de inicio.

Preguntas frecuentes

¿Qué firma utiliza WayForPay ?

HMAC_MD5 sobre campos seleccionados unidos con punto y coma, utilizando la clave secreta del comerciante. El orden de los campos es fijo y difiere para el formulario de pago, la devolución de llamada y la respuesta de devolución de llamada.

¿Qué debe devolver el punto final serviceUrl?

Una respuesta JSON con orderReference, el estado "aceptar", la hora actual de Unix y una firma calculada como HMAC_MD5 de "orderReference;accept;time". Sin él, WayForPay vuelve a intentar la devolución de llamada.

¿Puedo confiar en la página returnUrl para marcar un pedido como pagado?

No. El navegador del cliente puede abrir esa URL sin pagar. Marque los pedidos como pagados solo después de una devolución de llamada de serviceUrl verificada o una respuesta CHECK_STATUS con un estado Aprobado y un monto y moneda coincidentes.

¿Cómo verifico el estado de un pedido WayForPay ?

Envíe una solicitud CHECK_STATUS firmada a la API WayForPay con su cuenta de comerciante y la referencia del pedido. Úselo en la página de devolución y en un trabajo cron que vuelva a verificar los pedidos atascados en espera.

¿Por qué WayForPay dice que la firma no es válida?

Por lo general, el formato del importe difiere entre la firma y el formulario (50 frente a 50,00), el nombre de un producto se modificó después de la firma, el orden de los campos es incorrecto o se utiliza la clave secreta incorrecta.

Llamada de descubrimiento gratuita

Traiga una pila desordenada o una integración estancada - Integrono trazará el primer paso más inteligente.

Obtiene una visión senior sobre trampas de presupuesto, dependencias y si conviene comprar, ensamblar o construir - antes de que empiece el trabajo de pago. El CEO sigue al tanto.