Merchant API
Проверка…Песочница
Руководство
Markdown-версия

Вебхуки

Уведомление на ваш сервер, когда заказ получает окончательный статус.

Передайте webhookUrl в теле POST /purchase. Когда заказ станет completed, failed или cancelled, мы отправим на этот адрес POST-запрос:

JSON
{
  "event": "order.status",
  "orderId": 1842,
  "status": "completed",
  "failureReason": null,
  "productId": 12,
  "price": 1490,
  "currency": "RUB",
  "idempotencyKey": "order-7731",
  "createdAt": "2026-11-13T12:36:53.000Z"
}

Ключа товара в вебхуке нет — получите его через GET /orders/{orderId}.

Заголовки

ЗаголовокЗначение
X-Merchant-TimestampВремя отправки, Unix-секунды.
X-Merchant-Signaturesha256= + hex HMAC-SHA256 от строки <timestamp>.<тело запроса> на вашем секрете вебхуков. Секрет выдаём вместе с ключом доступа.
User-Agentmerchant-webhooks/1

Проверка подписи

Всегда проверяйте подпись и время: иначе кто угодно сможет прислать вам «оплаченный» заказ.

JavaScript
import crypto from "node:crypto";

// rawBody — тело запроса строкой, ДО JSON.parse: подпись считается по байтам.
export function verifyMerchantWebhook(rawBody, headers, secret) {
  const timestamp = headers["x-merchant-timestamp"];
  const signature = headers["x-merchant-signature"] ?? "";
  // Отбрасываем старые доставки — защита от повторной отправки перехваченного запроса.
  if (!timestamp || Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;

  const expected =
    "sha256=" + crypto.createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest("hex");
  const a = Buffer.from(signature);
  const b = Buffer.from(expected);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

// Express:
// app.post("/hooks/merchant", express.raw({ type: "application/json" }), (req, res) => {
//   if (!verifyMerchantWebhook(req.body.toString("utf8"), req.headers, process.env.MERCHANT_WEBHOOK_SECRET))
//     return res.sendStatus(401);
//   const event = JSON.parse(req.body);
//   res.sendStatus(200); // ответьте быстро, обработку — в очередь
// });
Python
import hashlib
import hmac
import time


def verify_merchant_webhook(raw_body: bytes, headers, secret: str) -> bool:
    timestamp = headers.get("X-Merchant-Timestamp", "")
    signature = headers.get("X-Merchant-Signature", "")
    if not timestamp.isdigit() or abs(time.time() - int(timestamp)) > 300:
        return False
    message = timestamp.encode() + b"." + raw_body
    expected = "sha256=" + hmac.new(secret.encode(), message, hashlib.sha256).hexdigest()
    return hmac.compare_digest(signature, expected)

Доставка и повторы

  • Ответ 2xx в течение 5 секунд — доставлено. Отвечайте сразу, а обработку ставьте в очередь.
  • Ошибка сети, таймаут, 5xx, 408, 429 — повтор через 30 с, 2 мин, 10 мин, 30 мин, 2 ч и 6 ч (всего до 7 попыток).
  • Редиректы (3xx) не выполняются, остальные 4xx считаются окончательным отказом — без повторов.
  • Итог виден в поле webhookStatus заказа: pending, delivered или failed.
  • Одно событие может прийти больше одного раза. Обрабатывайте идемпотентно — по паре orderId + status.

Требования к URL

Только https://, до 2048 символов, без логина и пароля в адресе. Нельзя localhost, внутренние и служебные IP-адреса — ни напрямую, ни через DNS-имя, которое на них указывает. Неверный адрес — 400 Invalid webhookUrl (причина).

В песочнице вебхук приходит только для успешной тестовой покупки, содержит "sandbox": true и повторяется до 3 раз.