Как подключить ЮKassa (Яндекс.Касса) к PHP-сайту в 2015 году
Коротко: Получите shopId и scid в личном кабинете Яндекс.Кассы, создайте HTML-форму с параметрами заказа и подписью MD5, POST на https://money.yandex.ru/wd/api/payform. Обрабатывайте callback на /payment/notify - проверяйте MD5 подпись, меняйте статус заказа.
Что такое Яндекс.Касса в 2015 году
Яндекс.Касса (ныне ЮKassa) в 2015 - агрегатор платежей: принимала Яндекс.Деньги, банковские карты, QIWI, WebMoney, Сбербанк Онлайн, терминалы. Одна интеграция - все методы оплаты. Комиссия: 2.8-3.5% в зависимости от метода и оборота.
Параметры формы оплаты
<?php
// payment/YandexKassa.php
class YandexKassa {
private string $shopId;
private string $scid;
private string $shopPassword; // Пароль 1 или пароль 2
public function __construct(string $shopId, string $scid, string $shopPassword) {
$this->shopId = $shopId;
$this->scid = $scid;
$this->shopPassword = $shopPassword;
}
/**
* Генерировать HTML-форму для перенаправления на оплату
*/
public function createForm(array $order): string {
$params = [
'shopId' => $this->shopId,
'scid' => $this->scid,
'sum' => number_format($order['total'], 2, '.', ''),
'customerNumber' => $order['user_id'],
'orderNumber' => $order['id'],
'shopSuccessURL' => 'https://myshop.ru/order/' . $order['id'] . '/success',
'shopFailURL' => 'https://myshop.ru/order/' . $order['id'] . '/fail',
'shopSendEmailNotification' => 0,
'cps_email' => $order['email'] ?? '', // Предзаполнить email покупателя
'paymentType' => '', // Пусто = показать все методы
];
$html = '<form method="POST" action="https://money.yandex.ru/wd/api/payform" id="ykassa-form">' . "\n";
foreach ($params as $name => $value) {
$html .= '<input type="hidden" name="' . htmlspecialchars($name) . '" value="' . htmlspecialchars($value) . '">' . "\n";
}
$html .= '<button type="submit">Оплатить</button>' . "\n";
$html .= '</form>' . "\n";
// Автосабмит если форма показывается на промежуточной странице
// $html .= '<script>document.getElementById("ykassa-form").submit();</script>';
return $html;
}
/**
* Проверить подпись уведомления от Яндекс.Кассы
* Вызывается на URL /payment/notify
*/
public function verifyNotification(array $post): bool {
$required = ['action', 'orderSumAmount', 'orderSumCurrencyPaycash',
'orderSumBankPaycash', 'shopId', 'invoiceId', 'customerNumber'];
foreach ($required as $field) {
if (!isset($post[$field])) return false;
}
// Подпись: MD5 от конкатенации полей + пароль
$signStr = implode(';', [
$post['action'],
$post['orderSumAmount'],
$post['orderSumCurrencyPaycash'],
$post['orderSumBankPaycash'],
$post['shopId'],
$post['invoiceId'],
$post['customerNumber'],
$this->shopPassword,
]);
$expectedMd5 = strtoupper(md5($signStr));
$receivedMd5 = strtoupper($post['md5'] ?? '');
return hash_equals($expectedMd5, $receivedMd5);
}
}
Обработчик уведомлений (Callback)
<?php
// public/payment/notify.php
// Яндекс.Касса вызывает этот URL после каждого платежа
require_once '../../bootstrap.php';
$ykassa = new YandexKassa(YKASSA_SHOP_ID, YKASSA_SCID, YKASSA_PASSWORD);
$post = $_POST;
$action = $post['action'] ?? '';
// Яндекс.Касса посылает два типа уведомлений:
// checkOrder - перед платежом (проверить что заказ существует)
// paymentAviso - после успешного платежа
if ($action === 'checkOrder') {
$orderId = (int)($post['orderNumber'] ?? 0);
$order = OrderRepository::find($orderId);
if (!$order) {
echo buildResponse(100, $post['invoiceId'] ?? '', 'Order not found');
exit;
}
// Проверяем что сумма совпадает
$expectedAmount = number_format($order['total'], 2, '.', '');
$receivedAmount = $post['orderSumAmount'] ?? '0';
if (abs((float)$expectedAmount - (float)$receivedAmount) > 0.01) {
echo buildResponse(100, $post['invoiceId'] ?? '', 'Amount mismatch');
exit;
}
echo buildResponse(0, $post['invoiceId'] ?? '', 'OK');
exit;
}
if ($action === 'paymentAviso') {
// Проверяем подпись
if (!$ykassa->verifyNotification($post)) {
echo buildResponse(1, $post['invoiceId'] ?? '', 'Bad signature');
exit;
}
$orderId = (int)($post['orderNumber'] ?? 0);
$order = OrderRepository::find($orderId);
if ($order && $order['status'] === 'pending_payment') {
OrderRepository::updateStatus($orderId, 'paid');
PaymentRepository::record([
'order_id' => $orderId,
'method' => 'yandex_kassa',
'transaction_id' => $post['invoiceId'],
'amount' => $post['orderSumAmount'],
'payment_type' => $post['paymentType'] ?? 'unknown',
'paid_at' => date('Y-m-d H:i:s'),
]);
// Отправить письмо клиенту
Mailer::sendOrderPaid($order);
}
echo buildResponse(0, $post['invoiceId'] ?? '', 'OK');
exit;
}
echo buildResponse(200, '', 'Unknown action');
function buildResponse(int $code, string $invoiceId, string $message): string {
// Яндекс.Касса ожидает XML-ответ строго в этом формате
return '<?xml version="1.0" encoding="UTF-8"?>' . "\n"
. '<' . ($_POST['action'] ?? 'checkOrder') . 'Response '
. 'performedDatetime="' . date('c') . '" '
. 'code="' . $code . '" '
. 'invoiceId="' . htmlspecialchars($invoiceId) . '" '
. 'message="' . htmlspecialchars($message) . '" '
. 'shopId="' . YKASSA_SHOP_ID . '"/>';
}
Таблица платежей
CREATE TABLE payments (
id INT AUTO_INCREMENT PRIMARY KEY,
order_id INT NOT NULL,
method VARCHAR(50) DEFAULT 'yandex_kassa',
payment_type VARCHAR(50), -- 'PC' = Яндекс.Деньги, 'AC' = банковская карта
transaction_id VARCHAR(100), -- invoiceId из Яндекс.Кассы
amount DECIMAL(10,2) NOT NULL,
currency CHAR(3) DEFAULT 'RUB',
status ENUM('pending','completed','failed','refunded') DEFAULT 'pending',
raw_request TEXT, -- Сохраняем POST данные от ЯК
paid_at DATETIME,
created_at DATETIME DEFAULT NOW(),
INDEX idx_order_id (order_id),
INDEX idx_transaction (transaction_id)
) ENGINE=InnoDB;
Типы оплаты (paymentType) в 2015
| Код | Метод |
|---|---|
PC |
Яндекс.Деньги |
AC |
Банковская карта |
MC |
Мобильный банк |
WM |
WebMoney |
SB |
Сбербанк Онлайн |
MP |
Мобильный платёж (SMS) |
AB |
Альфа-Клик |
MA |
MasterPass |
PB |
Промсвязьбанк |
QW |
QIWI |
GP |
Терминалы (Contact/Svyaznoy) |
Тестирование без реальных денег
// В личном кабинете Яндекс.Кассы включить тестовый режим
// Тестовые данные карты:
// Номер: 5555555555554444
// Срок: любой будущий
// CVV: 123
// 3D Secure пароль: 12345678
// Логировать все уведомления в тестовом режиме
file_put_contents(
'/var/log/ykassa_callbacks.log',
date('Y-m-d H:i:s') . ' ACTION:' . ($_POST['action'] ?? '') .
' ORDER:' . ($_POST['orderNumber'] ?? '') .
' AMOUNT:' . ($_POST['orderSumAmount'] ?? '') . "\n" .
print_r($_POST, true) . "\n---\n",
FILE_APPEND
);
Частые ошибки интеграции
Код 1 (bad signature): Убедитесь что используете пароль магазина (не пароль от личного кабинета). В настройках: «Мои магазины» → «Показать пароли».
Яндекс.Касса не вызывает callback: URL должен быть доступен извне. На localhost не работает - используйте ngrok для локального тестирования.
Код 100 (order not found): orderNumber в уведомлении - строка, а в базе - INT. Всегда приводить к нужному типу при сравнении.