Webhooks

When a payment ends, ST Pay sends one POST request to the callbackUrl you gave when you created the payment or the payment link. Use it to mark the order paid; do not rely only on the buyer returning to your success page, because the buyer can close the tab.

When it is sent

What you receive

Content-Type: application/json, the same body as the payment response:

{
  "id": "6f1c0c7e-2b64-4f0e-9b1d-0a6c2c9b7d11",
  "orderReference": "order-1042",
  "amount": 1500.5,
  "currency": "KZT",
  "paymentMethod": "card",
  "success": true,
  "captured": true,
  "cardMask": "424242******4242",
  "cardScheme": "visa"
}

Use id to find the payment, orderReference to find your order, and success for the result.

Check that it came from ST Pay

Every request carries an X-Signature header: a hex HMAC-SHA256 made with your store's private key (pr_…, the whole value as you copied it from the dashboard).

The signed text is these five values written one after another with nothing between them:

id + amount + currency + orderReference + success

amount is written the shortest way (1500.5, 1500, not 1500.50 or 1500.0), and success is true or false.

import crypto from "node:crypto";

function isFromStPay(req, rawBody, privateKey) {
  const p = JSON.parse(rawBody);
  const text = `${p.id}${p.amount}${p.currency}${p.orderReference}${p.success}`;
  const expected = crypto.createHmac("sha256", privateKey).update(text).digest("hex");
  const got = String(req.headers["x-signature"] || "");
  return got.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(got), Buffer.from(expected));
}

Reject a request whose signature does not match, and compare in constant time, as above.

Your answer and retries