Integração WayForPay em PHP: formulário de pagamento, assinatura e retorno de chamada
WayForPay é a maneira prática de aceitar pagamentos com cartão em UAH, USD e EUR de uma empresa ucraniana. Este guia percorre toda a integração em PHP: assinando o formulário de pagamento, recebendo o callback do servidor, respondendo corretamente e confirmando o status do pedido.
Em resumo
- O fluxo tem três partes: um formulário de pagamento assinado que envia o cliente para WayForPay, um retorno de chamada de servidor para servidor (
serviceUrl) e sua própria página de status do pedido. - Cada solicitação e resposta é assinada com HMAC_MD5 usando sua chave secreta do comerciante; a ordem do campo é importante.
- Marque um pedido como pago somente a partir de um retorno de chamada verificado ou de uma resposta
CHECK_STATUS, nunca do retorno do cliente ao seu site. - Você deve responder ao retorno de chamada com uma resposta JSON "aceitar" assinada, caso contrário, WayForPay continuará tentando novamente.
- Mantenha valores, moedas e referências de pedidos em seu banco de dados e compare-os com o que diz o retorno de chamada.
Uma integração WayForPay em PHP é uma pequena quantidade de código, mas tem vários lugares onde um erro silencioso significa pedidos não pagos marcados como pagos ou pedidos pagos que nunca são atendidos. Executamos o checkout WayForPay em nossos próprios sites, incluindo os serviços pagos neste site, e o conectamos a várias lojas de clientes. Abaixo está a estrutura que usamos, com os detalhes que a documentação lista, mas as pessoas tendem a ignorar.
Como funciona o fluxo de pagamento
- O cliente clica em "Pagar". Seu servidor cria um pedido no banco de dados com um
orderReferenceexclusivo, um valor fixo e uma moeda. - Seu servidor cria os campos do formulário de pagamento, assina-os e renderiza um formulário HTML de envio automático que é postado na página de pagamento WayForPay.
- O cliente paga na página WayForPay (cartão, Apple Pay, Google Pay e outros métodos habilitados em sua conta de comerciante).
- WayForPay chama seu
serviceUrlcom o resultado da transação. Esta chamada de servidor para servidor é o que você confia. - O cliente é redirecionado para o seu
returnUrl, onde você mostra o status atual do pedido.
A etapa 5 pode acontecer antes do término da etapa 4, portanto, a página de retorno deve dizer "confirmando seu pagamento" até que o retorno de chamada seja processado.
O que você precisa da conta do comerciante
merchantAccount- seu login de comerciante.merchantSecretKey- o segredo usado para assinaturas. Mantenha-o em uma variável de ambiente ou em um arquivo de configuração não rastreado, nunca no git ou no código front-end.merchantDomainName- o domínio do seu site, conforme registrado na conta.- As moedas e métodos de pagamento habilitados para sua conta.
Passo 1. Crie e assine o formulário de pagamento
A assinatura é um HMAC com MD5 sobre campos selecionados unidos por ponto e vírgula. Para uma compra, o pedido é: conta do comerciante, nome de domínio do comerciante, referência do pedido, data do pedido (carimbo de data e hora Unix), valor, moeda, todos os nomes de produtos, todas as contagens de produtos e todos os preços dos produtos.
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);
Em seguida, renderize o formulário. Os campos de produto são matrizes, portanto usam o sufixo []:
<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>
Os erros de assinatura mais comuns são: um formato de número diferente na assinatura e no formulário (50 vs 50.00), um nome de produto que é alterado (cortado, com escape de HTML) após a assinatura e esquecer que a ordem dos campos é fixa.
Etapa 2. Receba o retorno de chamada em serviceUrl
Quando o pagamento atinge o estado final, WayForPay publica o resultado em serviceUrl. Leia o corpo bruto (é JSON; algumas configurações também entregam campos de formulário, então lide com ambos) e verifique a assinatura. Para o callback é computado: conta do comerciante, referência do pedido, valor, moeda, código de autorização, máscara do cartão, status da transação e 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;
}
Use hash_equals para comparação para evitar vazamentos de tempo. Somente após a assinatura ser válida você deverá consultar o pedido e comparar amount e currency com o que armazenou. Se forem diferentes, registre-o e não marque o pedido como pago.
Passo 3. Atualize o pedido e responda corretamente
O campo transactionStatus informa o que aconteceu: Approved significa que o dinheiro foi capturado; Declined, Expired e valores semelhantes significam falha; Pending ou InProcessing significa espera; Refunded e Voided significa que o dinheiro foi devolvido. Atualize seu pedido adequadamente, em uma transação, e torne-o idempotente: se o pedido já estiver pago, não faça nada e ainda assim responda com sucesso.
WayForPay requer uma resposta assinada para saber que você recebeu a mensagem. A assinatura está acima da referência do pedido, da palavra accept e da hora atual:
$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);
Se o seu servidor não responder assim, WayForPay tratará a entrega como falha e tentará novamente, o que significa que você receberá o mesmo retorno de chamada várias vezes. Essa é outra razão para a verificação de idempotência.
Passo 4. Confirme o status com CHECK_STATUS
Os retornos de chamada podem ser perdidos: seu servidor caiu, um firewall bloqueou a solicitação, uma implantação quebrou a rota. WayForPay oferece uma chamada de API CHECK_STATUS que retorna o estado atual de um pedido por sua referência. Use-o em dois lugares: na página de retorno, se o retorno de chamada não chegar após alguns segundos, e em um pequeno cron job que verifica novamente os pedidos presos em "pendentes" por mais de, digamos, quinze minutos. A solicitação é assinada com HMAC_MD5 na conta do comerciante e na referência do 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 as mesmas verificações de um retorno de chamada: verifique a assinatura da resposta quando presente, compare o valor e a moeda e atualize o pedido de forma idempotente.
Testando antes de ir ao ar
- Use o modo de teste e os cartões de teste da documentação WayForPay até que um pagamento integral, uma recusa e um reembolso sejam vistos em seus registros.
- Seu
serviceUrldeve ser acessível pela Internet por HTTPS. No host local, use um túnel ou implante em um domínio temporário. - Registre cada retorno de chamada com seu corpo bruto, o resultado da verificação e o resultado. Nunca registre a chave secreta.
- Certifique-se de que o WAF ou a proteção de bot (por exemplo, no Cloudflare) não desafie ou bloqueie as solicitações de retorno de chamada do WayForPay.
Erros comuns
- Confiando no URL de retorno. O cliente pode abri-lo sem pagar.
- Reutilizando uma referência de pedido. Cada tentativa precisa de uma referência única, ou você confundirá as tentativas.
- Alterar o valor após assinar, ou mostrar um preço e assinar outro.
- Não respondendo ao retorno de chamada com a resposta de aceitação assinada.
- Marque como pago antes de verificar o valor e a moeda.
- Várias moedas, um segredo compartilhado e nenhum mapeamento: mantenha a moeda na ordem e verifique-a no retorno de chamada.
Como podemos ajudar
Construímos integrações de pagamento de ponta a ponta: WayForPay, Stripe, PayPal, faturamento recorrente, reembolsos e relatórios de reconciliação. Se você já tem um checkout funcionando, mas não tem certeza sobre seus modos de falha, agende uma análise de arquitetura e revisaremos o tratamento de retorno de chamada, idempotência e registro. Para obter um guia semelhante sobre outro provedor, consulte Stripe webhooks em PHP e, para a abordagem geral, leia nosso manual de integrações de inicialização.
Perguntas frequentes
Qual assinatura WayForPay usa?
HMAC_MD5 sobre campos selecionados unidos por ponto e vírgula, usando a chave secreta do comerciante. A ordem dos campos é fixa e difere para o formulário de pagamento, o retorno de chamada e a resposta de retorno de chamada.
O que o endpoint serviceUrl deve retornar?
Uma resposta JSON com orderReference, o status "accept", o horário Unix atual e uma assinatura calculada como HMAC_MD5 de "orderReference;accept;time". Sem ele, WayForPay tenta novamente o retorno de chamada.
Posso confiar na página returnUrl para marcar um pedido como pago?
Não. O navegador do cliente pode abrir esse URL sem pagar. Marcar pedidos como pagos somente após um retorno de chamada serviceUrl verificado ou uma resposta CHECK_STATUS com status Aprovado e valor e moeda correspondentes.
Como posso verificar o status de um pedido WayForPay ?
Envie uma solicitação CHECK_STATUS assinada para a API WayForPay com sua conta de comerciante e a referência do pedido. Use-o na página de retorno e em um cron job que verifica novamente os pedidos pendentes.
Por que WayForPay diz que a assinatura é inválida?
Normalmente o formato do valor difere entre a assinatura e o formulário (50 vs 50,00), o nome de um produto foi alterado após a assinatura, a ordem dos campos está errada ou a chave secreta errada é usada.
Chamada de descoberta gratuita
Traga uma stack desordenada ou uma integração parada - a Integrono mapeia o primeiro passo mais inteligente.
Recebe uma leitura senior sobre armadilhas de orçamento, dependências e se convém comprar, colar ou construir - antes de começar o trabalho pago. O CEO permanece no loop.