Платежи Вебхуки

Вебхуки (уведомления)

Когда статус платежа меняется на completed, failed, cancelled, expired или amount_mismatch, наша система автоматически отправляет POST-запрос на URL, указанный в настройках кассы. Это позволяет вашему серверу узнавать о результатах платежей в реальном времени.

Метод POST
Content-Type application/json
URL Задаётся в настройках кассы (webhook_url)

Когда отправляется вебхук

completed

Платёж завершён — деньги получены, платёж успешно проведён

failed

Платёж отклонён — не удалось провести платёж

cancelled

Платёж отменён — плательщик или мерчант отменил платёж до его проведения

expired

Платёж истёк — реквизиты не были оплачены в отведённое время

amount_mismatch

Средства поступили, но сумма отличается от ожидаемой — поле fact_amount содержит фактически поступившую сумму. Платёж заморожен до решения администратора; после решения придёт финальный вебхук (completed или failed) с old_status = amount_mismatch

Заголовки запроса

Каждый вебхук содержит два заголовка:

Заголовок Описание
Content-Type Всегда application/json
X-Webhook-Signature HMAC-SHA256 подпись тела запроса. Используется для проверки подлинности

Тело запроса (Payload)

Пример тела запроса:

JSON
{
    "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 — средства поступили не полностью:

JSON
{
    "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 Ключ идемпотентности — для защиты от дублей платежей

Проверка подлинности (верификация подписи)

Обязательно проверяйте подпись каждого вебхука. Это гарантирует, что запрос отправлен нашей системой, а не злоумышленником.

Как формируется подпись

  1. Тело запроса (raw JSON) кодируется в строку.
  2. Вычисляется HMAC-SHA256 от этой строки с использованием секретного ключа кассы (secret_key).
  3. Результат (hex-строка) помещается в заголовок X-Webhook-Signature.

Формула

signature = HMAC-SHA256(тело_запроса, secret_key)

тело_запроса — сырая JSON-строка в точности как получена (без переформатирования).
secret_key — секретный ключ вашей кассы, доступен в личном кабинете.

Пример верификации на PHP

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

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

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 часов.

Защита от дублей

Мы можем отправить один и тот же вебхук более одного раза (например, при таймауте). Рекомендации:

1

Используйте payment_id как идемпотентный ключ

Если вы уже обработали платёж с таким ID — просто верните 200 OK, не повторяя бизнес-логику.

2

Проверяйте status

Обновляйте статус заказа только если он отличается от текущего.

PHP — защита от дублей
$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)

webhook-handler.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';

Пошаговая интеграция

1

Создайте кассу в личном кабинете, получите secret_key

2

Укажите webhook_url в настройках кассы — URL на вашем сервере. URL должен быть публично доступен и возвращать HTTP 200 на GET-запрос.

3

Реализуйте обработчик: читает тело → проверяет подпись → декодирует JSON → обрабатывает статус → отвечает 200

4

Протестируйте, создав тестовый платёж через API

5

Убедитесь, что обработчик отвечает быстрее 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 указывает на рабочий эндпоинт.