Webhook по платежу
При смене статуса на ваш callbackUri уходит POST с JSON. Смотрите data.status. Тот же платёж, что в create и GET статуса. Общие правила доставки — в Webhooks.
Подпись
Запрос подписывается тем же секретом API-ключа, что и ваши запросы к нам. Секрет в webhook не передаётся — только подпись.
| Поле | Тип | Обяз. | Описание |
|---|---|---|---|
| Signature | header | да | HMAC-SHA256 от сырого тела POST, hex в нижнем регистре. |
| X-Merchant-Key-Id | header | да | Id ключа, которым подписан webhook. |
- Алгоритм:
HMAC-SHA256, ключ = секрет API-строки. - Сообщение = точные байты JSON-тела (как пришло в запросе). Не пересобирайте JSON перед проверкой.
- Схема та же, что при аутентификации входящих запросов.
Пример проверки (Node.js):
javascript
import crypto from "crypto";
function verifyWebhook(rawBody, secret, signatureHeader) {
const expected = crypto
.createHmac("sha256", secret)
.update(rawBody, "utf8")
.digest("hex");
const got = String(signatureHeader || "").toLowerCase();
if (expected.length !== got.length) return false;
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(got));
}
// Нужен сырой body (байты как в запросе), не уже распарсенный JSON.
app.post("/callback", express.raw({ type: "application/json" }), (req, res) => {
const raw = req.body.toString("utf8");
const ok = verifyWebhook(raw, process.env.MERCHANT_SECRET, req.get("Signature"));
if (!ok) return res.status(401).end();
const event = JSON.parse(raw);
// дедуп по eventId; обновить заказ по data.orderId / data.status
res.status(200).end();
});Пример проверки (PHP):
php
$raw = file_get_contents('php://input');
$expected = hash_hmac('sha256', $raw, $secret);
$got = strtolower($_SERVER['HTTP_SIGNATURE'] ?? '');
$ok = hash_equals($expected, $got);Поля
| Поле | Тип | Обяз. | Описание |
|---|---|---|---|
| type | string | да | Сейчас всегда payment.status.updated. |
| eventId | string | да | Id события. По нему отсекайте повторы. |
| createdAt | string | да | Время события (UTC). |
| data.id | string (UUID) | да | Id платежа (тот же, что в create). |
| data.orderId | string | да | Ваш orderId. |
| data.status | string | да | Статус в UPPERCASE. |
| data.amount / currency | string | да | Сумма и валюта. |
| data.extra | object | нет | Курсы/конверсия по платежу (если есть). |
| data.payerRequisites | object | нет | Только quasi_ecom: маска карты плательщика (cardLast4, опционально cardHolder). Полный PAN, CVV, OTP и срок действия не передаём. |
| id / order_id | string | нет | Дубликаты data.id и data.orderId на корне. |
| meta | object | нет | Справка (например reason). На бизнес-логику не опирайтесь. |
Статусы
PENDING → COMPLETED / FAILED / CANCELLED
PENDING— ждёт оплатыCOMPLETED— успехFAILED— неуспехCANCELLED— отменаAPPEAL— спорREQUISITE_UNAVAILABLE— реквизиты не выданы (только в GET / ответе create; webhook при невыдаче не отправляется)
Финал с webhook: COMPLETED, FAILED, CANCELLED. Статус REQUISITE_UNAVAILABLE приходит только в HTTP-ответе создания / GET — колбэк на callbackUri не шлётся, пока реквизит не был выдан мерчанту. В GET статуса те же значения, но строчными буквами.
Пример
json
{
"type": "payment.status.updated",
"eventId": "evt_01hz…",
"createdAt": "2026-04-20T12:00:00Z",
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"orderId": "demo-001",
"status": "COMPLETED",
"amount": "1000",
"currency": "RUB"
},
"id": "550e8400-e29b-41d4-a716-446655440000",
"order_id": "demo-001"
}Для quasi_ecom после ввода карты клиентом в data может быть payerRequisites (только last4 и имя; полный номер карты не отдаём):
json
{
"type": "payment.status.updated",
"eventId": "evt_01hz…",
"createdAt": "2026-07-31T12:00:00Z",
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"orderId": "az-order-001",
"status": "COMPLETED",
"amount": "50",
"currency": "AZN",
"payerRequisites": {
"type": "card",
"cardLast4": "1111",
"cardHolder": "Anar Rzayev"
}
},
"id": "550e8400-e29b-41d4-a716-446655440000",
"order_id": "az-order-001"
}Как обрабатывать
- Проверить заголовок Signature по сырому телу
- Пропустить уже виденный
eventId - Обновить заказ по
data.orderId/data.idиdata.status - На финальном статусе закрыть сценарий у себя и ответить 2xx