Блог

Integrations 7 хв читання

Інтеграція WayForPay на PHP: форма оплати, підпис та callback

WayForPay - практичний спосіб приймати карти в UAH, USD та EUR для українського бізнесу. У цьому гайді пройдемо всю інтеграцію на PHP: підпишемо платіжну форму, приймемо серверний callback, правильно відповімо на нього та підтвердимо статус замовлення.

Обкладинка статті: Інтеграція WayForPay на PHP: форма оплати, підпис та callback

Коротко

  • У потоці три частини: підписана форма, яка відправляє клієнта на WayForPay, серверний callback (serviceUrl) та ваша сторінка статусу замовлення.
  • Кожен запит та відповідь підписується HMAC_MD5 із секретним ключем мерчанта, порядок полів важливий.
  • Помічайте замовлення оплаченим лише за перевіреним callback або відповіддю CHECK_STATUS, але не з повернення клієнта на сайт.
  • На callback потрібно відповісти підписаним JSON-відповіддю accept, інакше WayForPay буде повторювати запит.
  • Зберігайте суму, валюту та номер замовлення у своїй базі та звіряйте їх з даними callback.

Інтеграція WayForPay на PHP - це трохи коду, але в кількох місцях тиха помилка призводить або до неоплачених замовлень, помічених оплаченими, або до оплачених замовлень, які ніхто не виконав. Ми використовуємо WayForPay на власних сайтах (у тому числі для платних послуг на цьому сайті) та підключали його до кількох магазинів клієнтів. Нижче – структура, якою ми користуємося, та деталі, які є в документації, але на які часто не звертають уваги.

Як влаштований платіжний потік

  1. Клієнт натискає "Сплатити". Сервер створює в базі замовлення з унікальним orderReference, фіксованою сумою та валютою.
  2. Сервер формує поля платіжної форми, підписує їх та віддає HTML-форму з автовідправкою на сторінку оплати WayForPay.
  3. Клієнт платить на сторінці WayForPay (карта, Apple Pay, Google Pay та інші методи, включені до кабінету мерчанта).
  4. WayForPay викликає ваш serviceUrl із результатом транзакції. Саме цьому серверному виклику ви довіряєте.
  5. Клієнта повертають на returnUrl, де ви показуєте поточний статус замовлення.

П'ятий крок може статися раніше завершення четвертого, тому сторінка повернення повинна показувати підтверджуємо оплату, поки callback не оброблений.

Що потрібно з кабінету Мерчанта

  • merchantAccount - логін мерчанта.
  • merchantSecretKey – секрет для підписів. Зберігайте його в змінній оточенні або в конфізі, що не відстежується, але не в git і не у фронтенді.
  • merchantDomainName – домен сайту, як він зазначений у кабінеті.
  • Включені для вашого облікового запису валюти та способи оплати.

Крок 1. Зберіть та підпишіть платіжну форму

Підпис - це HMAC з MD5 від вибраних полів, з'єднаних крапкою з комою. Для покупки порядок такий: merchant account, домен мерчанта, номер замовлення, дата замовлення (Unix timestamp), сума, валюта, потім усі назви товарів, усі кількості та усі ціни.

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);

Потім малюйте форму. Поля товарів - масиви, тому вони мають суфікс []:

<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>

Найчастіші помилки підпису: різний формат числа у підписі та у формі (50 і 50.00), назва товару, змінена після підпису (обрізана, екранована), та забутий фіксований порядок полів.

Крок 2. Прийміть callback на serviceUrl

Коли платіж досягає фінального стану, WayForPay відправляє результат на serviceUrl. Читайте сире тіло (це JSON, в деяких конфігураціях приходять поля форми, тому підтримайте обидва варіанти) і перевіряйте підпис. Для callback вона вважається за полями: merchant account, номер замовлення, сума, валюта, код авторизації, маска карти, статус транзакції та код причини.

$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;
}

Для порівняння використовуйте hash_equals щоб не було витоку через час. Тільки після валідного підпису шукайте замовлення і звіряйте amount і currency з тим, що ви зберегли. Якщо значення відрізняються, запишіть у балку і не помічайте замовлення оплаченим.

Крок 3. Оновіть замовлення та дайте відповідь правильно

Поле transactionStatus каже, що сталося: Approved - гроші списані; Declined, Expired та подібні - неуспіх; Pending або InProcessing – потрібно чекати; Refunded та Voided - гроші повернулися. Оновлюйте замовлення в транзакції і робіть це ідемпотентно: якщо замовлення вже оплачене, нічого не змінюйте, але все одно відповідайте успіхом.

WayForPay вимагає підписану відповідь, щоб знати, що ви отримали повідомлення. Підпис вважається від номера замовлення, слова accept та поточного часу:

$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);

Якщо сервер так не відповідає, WayForPay вважає доставку невдалою і повторює її, тому ви отримаєте той самий callback кілька разів. Це ще одна причина для перевірки ідемпотентності.

Крок 4. Підтвердьте статус через CHECK_STATUS

Callback можна пропустити: сервер лежав, файрвол заблокував запит, деплой зламав маршрут. WayForPay має виклик API CHECK_STATUS, який повертає поточний стан замовлення за його номером. Використовуйте його в двох місцях: на сторінці повернення, якщо callback не прийшов через кілька секунд, і в невеликому cron-завданні, яке перевіряє ще раз замовлення, що зависли в «очікує», скажімо, довше п'ятнадцяти хвилин. Запит підписується HMAC_MD5 від merchant account та номера замовлення.

$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);

Застосовуйте ті ж перевірки, що й для callback: перевіряйте підпис відповіді, якщо він є, звіряйте суму та валюту та оновлюйте замовлення ідемпотентно.

Тестування перед запуском

  • Використовуйте тестовий режим та тестові картки з документації WayForPay, доки в логах не з'явиться повний успішний платіж, відмова та повернення.
  • Ваш serviceUrl має бути доступний з Інтернету за HTTPS. Локально використовуйте тунель або викочуйте на staging-домен.
  • Логуйте кожен callback: сире тіло, результат перевірки та результат. Секретний ключ у логі не пишіть ніколи.
  • Переконайтеся, що WAF або захист від ботів (наприклад, Cloudflare) не блокує і не перевіряє капчею запити callback від WayForPay.

Часті помилки

  • Довіра до return URL. Клієнт може його відкрити, не заплативши.
  • Повторне використання номера замовлення. Для кожної спроби потрібний унікальний номер, інакше спроби переплутаються.
  • Зміна суми після підпису або показ однієї ціни під час підпису іншої.
  • Немає відповіді на callback підписаною відповіддю accept.
  • Позначка «оплачено» до перевірки суми та валюти.
  • Декілька валют, один секрет і ніякої прив'язки: зберігайте валюту в замовленні і перевіряйте її в callback.

Чим ми можемо допомогти

Ми робимо платіжні інтеграції під ключ: WayForPay, Stripe, PayPal, рекурентні платежі, повернення та звіти звірки. Якщо у вас вже працює оплата, але ви не впевнені в сценаріях збоїв, замовте архітектурний аналіз, і ми перевіримо обробку callback, ідемпотентність та логування. Схожий гайд по іншому провайдеру - Stripe webhooks на PHP, а загальний підхід описаний у нашому плейбуку інтеграцій для стартапів.

Часті запитання

Який підпис використовує WayForPay ?

HMAC_MD5 від вибраних полів, з'єднаних крапкою з комою, із секретним ключем мерчанта. Порядок полів фіксований та відрізняється для платіжної форми, callback та відповіді на callback.

Що має повернути endpoint serviceUrl?

JSON з orderReference, статусом «accept», поточним Unix-часом та підписом HMAC_MD5 від рядка «orderReference;accept;time». Без такої відповіді WayForPay повторює callback.

Чи можна по сторінці returnUrl позначати замовлення оплаченим?

Ні. Браузер клієнта може відкрити цю адресу, не заплативши. Позначайте замовлення оплаченим тільки після перевіреного callback на serviceUrl або відповіді CHECK_STATUS зі статусом Approved та сумою, що збігаються, і валютою.

Як перевірити статус замовлення WayForPay ?

Надішліть підписаний запит CHECK_STATUS в API WayForPay з вашим merchant account та номером замовлення. Використовуйте його на сторінці повернення і в cron-завданні, яке перевіряє ще раз замовлення в статусі pending.

Чому WayForPay пише, що підпис неправильний?

Зазвичай формат суми у підписі та у формі різниться (50 та 50.00), назву товару змінили після підпису, порядок полів неправильний або використовується не той секретний ключ.

Безкоштовний discovery-дзвінок

Хаотичний стек чи зависла інтеграція - Integrono намітить найрозумніший перший крок.

Ви отримуєте senior-погляд на бюджетні пастки, залежності і коли купувати, склеювати чи писати - до старту оплати. CEO лишається в контурі.