Webhook
Webhook — основной и единственный надёжный канал, по которому вы узнаёте об
оплате. Возврат плательщика на return_url подтверждением не является: вкладку могут
закрыть, а деньги при этом пройдут.
При каждой смене статуса мы отправляем POST на адрес webhook того
проекта, которому принадлежит платёж. Адрес задаётся на
странице проекта.
Заголовки
| Заголовок | Значение |
|---|---|
X-Signature | HMAC-SHA256 от сырого тела запроса, в hex. |
X-Key-Prefix | Первые 10 символов ключа, которым подписано. Нужен при ротации — см. ниже. |
X-Payment-Id | Идентификатор платежа. |
X-Webhook-Version | Версия формата, сейчас 2. |
Тело
{
"id": "uuid",
"external_order_id": "order-123",
"amount": "1000.00",
"amount_net": "820.00",
"fee": "180.00",
"status": "succeeded",
"is_test": false,
"paid_at": "2026-05-19T12:01:32Z",
"timestamp": "2026-05-19T12:01:32.512Z",
"nonce": "8f3a..."
} -
amount— сколько списано с плательщика (gross),amount_net— сколько получаете вы (gross минусfee). В режиме «комиссию платит плательщик» сверяйте заказ поamount_net: именно оно равно сумме, переданной в создание платежа. -
is_test—trueдля платежей, сделанных до активации аккаунта. Такой webhook нужно принять и проверить, но ни в коем случае не отгружать по нему заказ. timestampиnonce— защита от повторной отправки, см. ниже.
Проверка подписи
Подпись считается так:
X-Signature = HMAC_SHA256(key = hex(SHA256(api_key)), msg = raw_body) → hex То есть секретом HMAC служит не сам ключ, а его SHA-256-хэш в hex. Так сделано потому, что на нашей стороне хранится только этот хэш — сам ключ мы не знаем и не можем узнать.
🔴 Считайте подпись по сырому телу запроса, до разбора JSON. Если сначала распарсить и потом сериализовать обратно, порядок ключей и пробелы поменяются, и подпись не сойдётся — это самая частая ошибка интеграции.
Подписывается новейший активный ключ проекта. Поэтому при ротации, пока у
проекта есть несколько активных ключей, смотрите на X-Key-Prefix — он говорит,
против какого из них проверять.
Проверка в Node.js
import crypto from "node:crypto";
function verifyWebhook(rawBody, signature, apiKey) {
const signingSecret = crypto.createHash("sha256").update(apiKey).digest("hex");
const expected = crypto.createHmac("sha256", signingSecret)
.update(rawBody)
.digest("hex");
if (expected !== signature) return false;
// Replay defense: reject bodies older than 5 minutes.
const body = JSON.parse(rawBody);
const ts = Date.parse(body.timestamp);
if (Date.now() - ts > 5 * 60 * 1000) return false;
return true;
} Проверка в Python
import hmac, hashlib, json
from datetime import datetime, timezone
def verify_webhook(raw_body: bytes, signature: str, api_key: str) -> bool:
signing_secret = hashlib.sha256(api_key.encode()).hexdigest()
expected = hmac.new(signing_secret.encode(), raw_body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, signature):
return False
body = json.loads(raw_body)
ts = datetime.fromisoformat(body["timestamp"])
age = (datetime.now(timezone.utc) - ts).total_seconds()
return age <= 300 # reject if > 5 minutes old Защита от повтора
Внутри подписанного тела лежат timestamp (ISO 8601) и nonce.
Отвергайте тело старше 5 минут — иначе перехваченный однажды валидный запрос
можно будет проигрывать сколько угодно раз, и подпись у него будет правильная.
Что считается доставкой
Доставкой считается ответ 2xx. Всё остальное — повод повторить, включая
редиректы: 301 и 302 для нас не успех, мы по ним не
переходим.
Проверьте это отдельно. Эндпоинт, отвечающий редиректом (типичный случай — фреймворк уводит с/webhookна/webhook/или на https), выглядит рабочим в браузере и при этом не принимает ни одного уведомления. Обнаруживается это обычно по недоотгруженным заказам, а не по ошибке.
Расписание повторов — шесть попыток, пока не придёт 2xx:
| Попытка | Через |
|---|---|
| 1 | сразу |
| 2 | 30 секунд |
| 3 | 5 минут |
| 4 | 30 минут |
| 5 | 2 часа |
| 6 | 12 часов |
После шестой неудачной попытки доставка прекращается. Восстановить состояние можно запросом GET /api/v1/payments/{id}.
Обработчик должен быть идемпотентным
Один и тот же платёж может прийти к вам несколько раз: повтор после сетевого таймаута, когда ваш
ответ не дошёл до нас, — штатная ситуация. Ориентируйтесь на id платежа и на то,
что заказ уже обработан, а не на сам факт получения запроса.