Payment Docs
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 express
server.js
import 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 id from the order response together with your externalId.
  • Move status handling into your order model; only COMPLETED means the money arrived.

On this page