Order status and webhooks
Receive payment results via signed webhooks or poll the order by ID
There are two ways to learn the result of a payment:
- Webhooks (recommended): we send a signed
POSTrequest to your server on every status change. - Polling: you request the order by ID. Use it as a fallback, not as the primary channel.
Webhooks
Where notifications are sent
The webhook address is resolved in this order:
callbackUrlpassed when creating the order: applies to that order only;- the webhook URL from your merchant settings (ask your manager to set it).
Your endpoint must accept POST requests with a JSON body and respond with HTTP 200.
Notification format
POST {callbackUrl}
Content-Type: application/json
X-Signature: HMAC-SHA512(request body, API token) in hexThe body is the order object:
{
"id": "a1b2c3d4-e5f6-7g8h-i9j0-k1l2m3n4o5p6",
"status": "COMPLETED",
"purpose": "Payment for order #1234",
"amount": "1000.5",
"commission": "1.5",
"received": "999.0",
"currency": "RUB",
"paymentType": "SBP",
"shopId": "a1b2c3d4-e5f6-7g8h-i9j0-k1l2m3n4o5p6",
"terminalId": "a1b2c3d4-e5f6-7g8h-i9j0-k1l2m3n4o5p6",
"merchantId": "a1b2c3d4-e5f6-7g8h-i9j0-k1l2m3n4o5p6",
"externalId": "order_1234",
"externalUserId": "user_987",
"paymentLink": "https://example.com/payment?id=a1b2c3d4-e5f6-7g8h-i9j0-k1l2m3n4o5p6",
"successUrl": "https://example.com/success",
"payedAt": "2023-03-21T12:34:56Z",
"updatedAt": "2023-03-21T12:34:56Z",
"createdAt": "2023-03-21T12:34:56Z"
}Verify the signature
Every notification carries the X-Signature header: an HMAC-SHA512 hash of the raw request body in hex, computed with your API token as the secret. Reject requests whose signature does not match.
import crypto from "crypto";
// Your API token is the HMAC secret
const apiToken = process.env.API_TOKEN;
app.post("/webhooks/payments", (req, res) => {
const signature = req.headers["x-signature"];
const body = JSON.stringify(req.body);
// Calculate HMAC-SHA512 of the raw request body and compare in hex
const expected = crypto.createHmac("sha512", apiToken).update(body).digest("hex");
if (expected !== signature) {
return res.status(403).send("Invalid signature");
}
// Signature is valid — process the notification idempotently
const data = req.body;
if (data.status === "COMPLETED") {
console.log(`Order ${data.externalId} has been paid`);
}
res.status(200).send("OK");
});Webhook recommendations
- Always respond with HTTP
200to confirm receipt. If your server does not respond, we retry the notification. - Verify the
X-Signatureheader before processing the body. - Process notifications idempotently: the same notification may be delivered more than once.
- Match the notification to your records by
externalId, not by amount. - Handle every possible status value, including the unsuccessful ones.
If the same endpoint also receives payout notifications, distinguish them by the X-Type: PAYOUT_UPDATE header, see Payout webhooks.
Poll the order by ID
GET /v1/orders/{orderId}orderId is the id from the order creation response.
curl -X GET "https://api.riopay.online/v1/orders/a1b2c3d4-e5f6-7g8h-i9j0-k1l2m3n4o5p6" \
-H "X-Api-Token: YOUR_API_TOKEN"The response is the order object.
Poll sparingly, for example once every few minutes for orders that have not received a webhook. Frequent polling does not speed up the payment.
Order statuses
| Status | Final | Description |
|---|---|---|
CREATED | No | Order created, payment link not yet opened |
PENDING | No | Awaiting payment |
COMPLETED | Yes | Payment successful, funds received |
FAILED | Conditionally | Payment error |
CANCELED | Conditionally | Order canceled by the system |
EXPIRED | Conditionally | Order expired before it was paid |
BLOCKED | Conditionally | Transaction blocked by the bank |
REFUND | Yes | Payment refunded to the payer. Set after COMPLETED |
CHARGEBACK | Yes | Payment disputed by the payer and reversed by the bank. Set after COMPLETED |
FAILED, CANCELED, EXPIRED and BLOCKED are not strictly final. If the bank confirms the payment later because of an error on its side, the order moves to COMPLETED and the webhook for this change is sent automatically. Be ready to receive COMPLETED for an order you have already marked as unsuccessful.
Only COMPLETED confirms that the funds arrived. A completed order can later move to REFUND or CHARGEBACK when the money is returned to the payer: treat both as a reversal of the payment. FAILED, CANCELED, EXPIRED and BLOCKED are unsuccessful outcomes, but not strictly final: if the bank confirms the payment later because of an error on its side, the order moves to COMPLETED and you receive a webhook automatically. Do not reuse an unsuccessful order for a retry: create a new one.
Some fields may be null depending on the processing stage. Up-to-date request and response schemas are always available in the Swagger documentation (link in the top navigation).