Code examples
Node.js
Minimal Express server that creates orders and verifies webhooks
A minimal Express server with two routes: one creates an order and redirects the payer, the other receives webhooks and verifies the X-Signature header.
npm install expressimport express from "express";
import crypto from "node:crypto";
const API_URL = "https://api.riopay.online/v1";
const API_TOKEN = process.env.API_TOKEN; // issued by your manager
const app = express();
// Keep the raw body: the signature is computed over the exact bytes we receive
app.use(express.json({ verify: (req, _res, buf) => (req.rawBody = buf) }));
// 1. Create an order and redirect the payer to the payment page
app.post("/create-order", async (req, res) => {
const { amount, orderId, userId } = req.body;
const response = await fetch(`${API_URL}/orders`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Api-Token": API_TOKEN,
},
body: JSON.stringify({
amount: String(amount),
externalId: orderId,
externalUserId: userId,
purpose: `Payment for order ${orderId}`,
successUrl: "https://your-site.com/payment-success",
failUrl: "https://your-site.com/payment-failed",
callbackUrl: "https://your-site.com/webhooks/payments",
}),
});
if (!response.ok) {
return res.status(response.status).send(await response.text());
}
const order = await response.json();
// Store order.id next to your own orderId to match webhooks later
res.redirect(order.paymentLink);
});
// 2. Receive webhooks
app.post("/webhooks/payments", (req, res) => {
const expected = crypto
.createHmac("sha512", API_TOKEN)
.update(req.rawBody)
.digest("hex");
if (expected !== req.headers["x-signature"]) {
return res.status(403).send("Invalid signature");
}
const { status, externalId } = req.body;
// Webhooks may arrive more than once: make this idempotent
if (status === "COMPLETED") {
console.log(`Order ${externalId} paid`);
// mark the order as paid in your database
} else if (["REFUND", "CHARGEBACK"].includes(status)) {
console.log(`Order ${externalId} reversed: ${status}`);
// the money went back to the payer: mark the order as refunded
} else if (["FAILED", "CANCELED", "EXPIRED", "BLOCKED"].includes(status)) {
console.log(`Order ${externalId} finished with ${status}`);
// may still become COMPLETED later if the bank confirms the payment
}
res.status(200).send("OK");
});
app.listen(3000, () => console.log("Listening on :3000"));What to adapt
- Replace the hard-coded URLs with your own success, fail and webhook addresses.
- Persist the
idfrom the order response together with yourexternalId. - Move status handling into your order model; only
COMPLETEDmeans the money arrived.