Вебхуки (уведомления)
Когда статус платежа меняется на completed, failed, cancelled, expired или amount_mismatch, наша система автоматически отправляет POST-запрос на URL, указанный в настройках кассы. Это позволяет вашему серверу узнавать о результатах платежей в реальном времени.
| Метод | POST |
| Content-Type | application/json |
| URL | Задаётся в настройках кассы (webhook_url) |
Когда отправляется вебхук
Платёж завершён — деньги получены, платёж успешно проведён
Платёж отклонён — не удалось провести платёж
Платёж отменён — плательщик или мерчант отменил платёж до его проведения
Платёж истёк — реквизиты не были оплачены в отведённое время
Средства поступили, но сумма отличается от ожидаемой — поле fact_amount содержит фактически поступившую сумму. Платёж заморожен до решения администратора; после решения придёт финальный вебхук (completed или failed) с old_status = amount_mismatch
Заголовки запроса
Каждый вебхук содержит два заголовка:
| Заголовок | Описание |
|---|---|
Content-Type |
Всегда application/json |
X-Webhook-Signature |
HMAC-SHA256 подпись тела запроса. Используется для проверки подлинности |
Тело запроса (Payload)
Пример тела запроса:
{
"payment_id": "0191a2b3-c4d5-7e8f-9a0b-1c2d3e4f5a6b",
"status": "completed",
"old_status": null,
"amount": 1000.00,
"currency": "RUB",
"pay_amount": 1052.63,
"fact_amount": 1052.63,
"product_id": "order-12345",
"description": "Оплата заказа №12345",
"metadata": {
"user_id": "42",
"cart_items": ["SKU001", "SKU002"]
},
"idempotency_key": "order-12345-pay-1"
}
Пример вебхука amount_mismatch — средства поступили не полностью:
{
"payment_id": "0191a2b3-c4d5-7e8f-9a0b-1c2d3e4f5a6b",
"status": "amount_mismatch",
"old_status": null,
"amount": 1000.00,
"currency": "RUB",
"pay_amount": 1052.63,
"fact_amount": 950.00,
"product_id": "order-12345",
"description": "Оплата заказа №12345",
"metadata": {
"user_id": "42"
},
"idempotency_key": "order-12345-pay-1"
}
Описание полей
| Поле | Тип | Описание |
|---|---|---|
payment_id |
string (UUID) | Уникальный идентификатор платежа в нашей системе |
status |
string | Текущий статус: completed, failed, cancelled, expired или amount_mismatch |
old_status |
string / null | Предыдущий статус платежа. Заполнен при изменении статуса вручную (администратором или автоакцептом), при обычных переходах — null. Позволяет отличить повторную доставку статуса от реального изменения после ручного решения |
amount |
float | Исходная сумма, указанная при создании платежа |
currency |
string | Валюта платежа (например, RUB, USDT) |
pay_amount |
float | Фактическая сумма к оплате (с учётом комиссии) |
fact_amount |
float / null | Реально поступившая сумма. Заполнена при amount_mismatch (сумма, которая фактически пришла) и при completed (сумма, зачисленная мерчанту). null — пока платёж не завершён и сумма неизвестна |
product_id |
string / null | Ваш идентификатор заказа, переданный при создании платежа |
description |
string / null | Описание платежа, переданное при создании |
metadata |
object / null | Произвольные данные, переданные вами при создании платежа |
idempotency_key |
string / null | Ключ идемпотентности — для защиты от дублей платежей |
Проверка подлинности (верификация подписи)
Обязательно проверяйте подпись каждого вебхука. Это гарантирует, что запрос отправлен нашей системой, а не злоумышленником.
Как формируется подпись
- Тело запроса (raw JSON) кодируется в строку.
- Вычисляется HMAC-SHA256 от этой строки с использованием секретного ключа кассы (
secret_key). - Результат (hex-строка) помещается в заголовок
X-Webhook-Signature.
Формула
signature = HMAC-SHA256(тело_запроса, secret_key)
тело_запроса — сырая JSON-строка в точности как получена (без переформатирования).
secret_key — секретный ключ вашей кассы, доступен в личном кабинете.
Пример верификации на PHP
$secretKey = 'ваш_secret_key';
$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'];
$expected = hash_hmac('sha256', $payload, $secretKey);
if (hash_equals($expected, $signature)) {
// Подпись верна — обрабатываем
$data = json_decode($payload, true);
} else {
http_response_code(403);
exit;
}
Пример верификации на Python
import hmac, hashlib
secret_key = 'ваш_secret_key'
payload = request.body # Django: request.body / Flask: request.get_data()
signature = request.headers['X-Webhook-Signature']
expected = hmac.new(
secret_key.encode('utf-8'),
payload,
hashlib.sha256
).hexdigest()
if hmac.compare_digest(expected, signature):
data = json.loads(payload)
# обрабатываем...
Пример верификации на Node.js
const crypto = require('crypto');
const secretKey = 'ваш_secret_key';
const payload = request.rawBody; // сырая строка, не JSON!
const signature = request.headers['x-webhook-signature'];
const expected = crypto
.createHmac('sha256', secretKey)
.update(payload)
.digest('hex');
if (expected === signature) {
const data = JSON.parse(payload);
// обрабатываем...
}
Как должен отвечать ваш сервер
| Ответ | Результат |
|---|---|
| HTTP 200 – 299 | Вебхук считается успешно доставленным |
| Другой код / таймаут | Вебхук считается не доставленным, будет повторная попытка |
Ваш сервер должен ответить 200 OK в течение 10 секунд. Не выполняйте тяжёлую логику синхронно — примите запрос, верните 200, обработку делайте в фоне.
Повторные попытки доставки
Если ваш сервер не ответил 200 OK, система будет повторять отправку:
| Попытка | Задержка |
|---|---|
| 1 | Сразу |
| 2 | Через 30 секунд |
| 3 | Через 2 минуты |
| 4 | Через 10 минут |
| 5 | Через 30 минут |
Максимум 5 попыток в течение 24 часов.
Защита от дублей
Мы можем отправить один и тот же вебхук более одного раза (например, при таймауте). Рекомендации:
Используйте payment_id как идемпотентный ключ
Если вы уже обработали платёж с таким ID — просто верните 200 OK, не повторяя бизнес-логику.
Проверяйте status
Обновляйте статус заказа только если он отличается от текущего.
$paymentId = $data['payment_id'];
$status = $data['status'];
$existing = getOrder($paymentId);
if ($existing && $existing['status'] === $status) {
// Уже обработано — отвечаем 200
http_response_code(200);
exit;
}
// Обрабатываем
updateOrder($paymentId, $status);
Полный пример обработчика (PHP)
<?php
$secretKey = 'ваш_secret_key';
// 1. Сырое тело запроса
$payload = file_get_contents('php://input');
// 2. Проверяем подпись
$signature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';
$expected = hash_hmac('sha256', $payload, $secretKey);
if (!hash_equals($expected, $signature)) {
http_response_code(403);
exit('Invalid signature');
}
// 3. Декодируем JSON
$data = json_decode($payload, true);
// 4. Обрабатываем статус
switch ($data['status']) {
case 'completed':
completeOrder($data['payment_id'], $data['fact_amount']);
break;
case 'amount_mismatch':
// Деньги пришли не в той сумме — ждём решения платформы
flagOrderForReview($data['payment_id'], $data['fact_amount']);
break;
case 'failed':
case 'cancelled':
case 'expired':
failOrder($data['payment_id']);
break;
}
// 5. Отвечаем 200
http_response_code(200);
echo 'OK';
Пошаговая интеграция
Создайте кассу в личном кабинете, получите secret_key
Укажите webhook_url в настройках кассы — URL на вашем сервере. URL должен быть публично доступен и возвращать HTTP 200 на GET-запрос.
Реализуйте обработчик: читает тело → проверяет подпись → декодирует JSON → обрабатывает статус → отвечает 200
Протестируйте, создав тестовый платёж через API
Убедитесь, что обработчик отвечает быстрее 10 секунд
Частые вопросы
Что означает вебхук amount_mismatch?
Клиент оплатил реквизиты, но поступившая сумма (fact_amount) отличается от ожидаемой (pay_amount) больше допустимой погрешности. Платёж заморожен до решения администратора платформы. Если сумма подтверждается, придёт финальный вебхук completed (комиссии пересчитываются от fact_amount), если нет — failed с возвратом средств. Если у мерчанта включён автоакцепт и отклонение в пределах его погрешности, платёж завершается сразу вебхуком completed без промежуточного amount_mismatch.
Что если мой сервер был недоступен?
Вебхук будет отправлен повторно до 5 раз с нарастающей задержкой. Также вы всегда можете проверить текущий статус платежа через API.
Можно ли указать другой URL для конкретного платежа?
Да, при создании платежа можно указать webhook_url — он имеет приоритет над URL из настроек кассы.
Что хранить в metadata?
Любые данные для связи платежа с вашим заказом: ID пользователя, корзину, callback-URL. Эти данные возвращаются без изменений.
Насколько безопасна подпись?
HMAC-SHA256 — промышленный стандарт. Секретный ключ известен только вам и нашей системе. Без ключа подделать подпись невозможно.
Почему мой webhook_url отклонён?
При сохранении webhook_url в настройках кассы или указании его при создании платежа система проверяет, что URL:
- Не указывает на локальный или частный адрес (localhost, 127.0.0.1, 192.168.x.x и т.д.)
- Доступен из интернета — сервер должен вернуть HTTP 200 на GET-запрос
Убедитесь, что ваш сервер запущен и доступен, а URL указывает на рабочий эндпоинт.