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
- A payment reaches a final result: approved (
authorized/charged) or declined (rejected/failed). - A payment that is still waiting for the buyer (a 3-D Secure challenge) is not sent yet. It is sent once the buyer finishes.
- A payment without a
callbackUrlsends nothing.
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
- Answer any 2xx status quickly. The body is ignored.
- Anything else, or no answer within 60 seconds, counts as a failure. ST Pay tries again after 15 seconds, 1 minute, 5 minutes and 30 minutes, then stops.
- The same payment can therefore arrive more than once. Make your handler idempotent: if the order is already marked paid, answer 200 and do nothing.
- If you were down and missed it, ask for the payment:
GET /v1/stores/{storeId}/payments/search?transactionId=…returns its final state.