protopaysAPI

Webhook по платежу

При смене статуса на ваш callbackUri уходит POST с JSON. Смотрите data.status. Тот же платёж, что в create и GET статуса. Общие правила доставки — в Webhooks.

Подпись

Запрос подписывается тем же секретом API-ключа, что и ваши запросы к нам. Секрет в webhook не передаётся — только подпись.

ПолеТипОбяз.Описание
SignatureheaderдаHMAC-SHA256 от сырого тела POST, hex в нижнем регистре.
X-Merchant-Key-Idheaderда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);

Поля

ПолеТипОбяз.Описание
typestringдаСейчас всегда payment.status.updated.
eventIdstringдаId события. По нему отсекайте повторы.
createdAtstringдаВремя события (UTC).
data.idstring (UUID)даId платежа (тот же, что в create).
data.orderIdstringдаВаш orderId.
data.statusstringдаСтатус в UPPERCASE.
data.amount / currencystringдаСумма и валюта.
data.extraobjectнетКурсы/конверсия по платежу (если есть).
data.payerRequisitesobjectнетТолько quasi_ecom: маска карты плательщика (cardLast4, опционально cardHolder). Полный PAN, CVV, OTP и срок действия не передаём.
id / order_idstringнетДубликаты data.id и data.orderId на корне.
metaobjectнетСправка (например 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"
}

Как обрабатывать

  1. Проверить заголовок Signature по сырому телу
  2. Пропустить уже виденный eventId
  3. Обновить заказ по data.orderId / data.id и data.status
  4. На финальном статусе закрыть сценарий у себя и ответить 2xx