Merchant API
Checking…Sandbox

Webhooks

A notification to your server when an order reaches a final status.

Pass webhookUrl in the POST /purchase body. When the order becomes completed, failed or cancelled, we send a POST request to that URL:

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"
}

The webhook does not contain the product key — get it via GET /orders/{orderId}.

Headers

HeaderValue
X-Merchant-TimestampSend time, Unix seconds.
X-Merchant-Signaturesha256= + hex HMAC-SHA256 of the string <timestamp>.<request body> with your webhook secret. The secret is issued together with your access key.
User-Agentmerchant-webhooks/1

Verifying the signature

Always check the signature and the timestamp — otherwise anyone can send you a “paid” order.

JavaScript
import crypto from "node:crypto";

// rawBody is the request body as a string, BEFORE JSON.parse: the signature covers the exact bytes.
export function verifyMerchantWebhook(rawBody, headers, secret) {
  const timestamp = headers["x-merchant-timestamp"];
  const signature = headers["x-merchant-signature"] ?? "";
  // Drop old deliveries — protects against replaying an intercepted request.
  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); // answer fast, queue the processing
// });
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)

Delivery and retries

  • A 2xx response within 5 seconds means delivered. Answer right away and queue the processing.
  • Network error, timeout, 5xx, 408, 429 — retried after 30 s, 2 min, 10 min, 30 min, 2 h and 6 h (up to 7 attempts in total).
  • Redirects (3xx) are not followed; other 4xx are a final refusal with no retries.
  • The result is in the order's webhookStatus field: pending, delivered or failed.
  • One event can arrive more than once. Process idempotently by the orderId + status pair.

URL requirements

https:// only, up to 2048 characters, no username or password in the URL. No localhost, internal or reserved IP addresses — neither directly nor through a DNS name pointing at them. An invalid URL gives 400 Invalid webhookUrl (reason).

In the sandbox a webhook is sent only for a successful test purchase, contains "sandbox": true and is retried up to 3 times.