RozetkaPay

Платіжна сторінка за 15 хвилин

End-to-end інтеграція з платіжною сторінкою RozetkaPay — від запиту на оплату до обробки callback зі статусом платежу

Приклад End-to-end інтеграції з платіжною сторінкою RozetkaPay — від запиту на оплату до обробки callback зі статусом платежу.

Кроки здійснення оплати

  1. Отримати URL платіжної сторінки — створити платіж через RozetkaPay
  2. Редірект покупця на платіжну сторінку — покупець оплачує на стороні RozetkaPay
  3. Обробка callback — отримати callback, верифікувати підпис, оновити статус замовлення
  4. Відображення статусу оплати покупцю
sequenceDiagram
    participant C as Клієнт
    participant S as Ваш сервер
    participant RP as RozetkaPay

    C->>S: POST /checkout
    S->>RP: POST /api/payments/v1/new
    RP-->>S: { action.value: hosted URL }
    S-->>C: 302 Редірект → hosted URL
    C->>RP: Оплата на сторінці RozetkaPay
    RP-->>C: Редірект → result_url
    RP->>S: POST /rozetkapay/callback (async)
    S->>S: Перевірка підпису, оновлення замовлення
    S-->>RP: 200 OK
    C->>S: GET /order/:id/result
    S-->>C: Статус платежу

Крок 1. Створення платежу

// server.js
import express from "express";
const app = express();
app.use(express.json());
const RP_LOGIN = process.env.RP_LOGIN;
const RP_PASSWORD = process.env.RP_PASSWORD;
const BASE_URL = "https://api.rozetkapay.com";
const PUBLIC_URL = process.env.PUBLIC_URL; // напр. https://your-site.com
app.post("/checkout", async (req, res) => {
const externalId = `order_${Date.now()}`;
const auth = Buffer.from(`${RP_LOGIN}:${RP_PASSWORD}`).toString("base64");
const r = await fetch(`${BASE_URL}/api/payments/v1/new`, {
method: "POST",
headers: {
Authorization: `Basic ${auth}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
external_id: externalId,
mode: "hosted",
amount: req.body.amount,
currency: "UAH",
confirm: true,
callback_url: `${PUBLIC_URL}/rozetkapay/callback`,
result_url: `${PUBLIC_URL}/order/${externalId}/result`,
customer: { email: req.body.email },
}),
});
const data = await r.json();
if (!data.is_success) return res.status(400).json(data);
// збережіть external_id ↔ payment_id на своєму боці (БД)
await orders.create({ external_id: externalId, payment_id: data.id, status: "init" });
res.redirect(302, data.action.value);
});

Крок 2. Обробка Callback

RozetkaPay POST-ить JSON на callback_url після завершення оплати. Підпис у заголовку X-ROZETKAPAY-SIGNATURE розраховується за формулою:

base64url(sha1(password + base64url(body) + password))

Повна специфікація — у Callbacks / Webhooks.

import crypto from "node:crypto";
function b64url(buf) {
return Buffer.from(buf).toString("base64").replace(/\+/g, "-").replace(/\//g, "_");
}
function verifySignature(password, rawBody, signature) {
const body64 = b64url(rawBody);
const hash = crypto
.createHash("sha1")
.update(password + body64 + password)
.digest();
return b64url(hash) === signature;
}
// ВАЖЛИВО: потрібен raw body, а не розпарсений JSON
app.post("/rozetkapay/callback", express.raw({ type: "application/json" }), async (req, res) => {
const signature = req.header("X-ROZETKAPAY-SIGNATURE");
const raw = req.body.toString("utf8");
if (!verifySignature(RP_PASSWORD, raw, signature)) {
return res.status(401).send("invalid signature");
}
const payload = JSON.parse(raw);
const { external_id, status, payment_id } = payload;
// дедуплікація: якщо status вже "success" — пропускаємо
const order = await orders.findByExternalId(external_id);
if (order?.status === status) return res.status(200).send("ok");
await orders.update(external_id, { status, payment_id });
// відповідаємо 200 швидко, важку роботу робіть асинхронно
res.status(200).send("ok");
});

Крок 3. Result page

app.get("/order/:id/result", async (req, res) => {
const order = await orders.findByExternalId(req.params.id);
// callback може ще не прийти — покажіть pending і робіть polling на клієнті
if (!order || order.status === "init" || order.status === "pending") {
return res.send("<h1>Платіж обробляється…</h1>");
}
if (order.status === "success") {
return res.send("<h1>Дякуємо! Оплата пройшла</h1>");
}
res.send("<h1>Оплата не вдалась</h1>");
});

Тестування локально

callback_url має бути публічно доступний. На localhost підніміть тунель:

# ngrok
ngrok http 3000
# → https://abc123.ngrok-free.app
export PUBLIC_URL=https://abc123.ngrok-free.app

Потім оплатіть тестовою карткою, наприклад 4444555566661111 (3DS success).

Чеклист перед запуском

  • Ключі API у змінних середовища, не в коді
  • callback_url використовує HTTPS
  • Підпис callback верифікується до парсингу JSON
  • external_id унікальний і зберігається у базі даних
  • Дедуплікація callback: один payment_id → одна бізнес-дія
  • Повторні спроби на 5xx — з тим самим external_id
  • BASE_URL переключено на https://api.rozetkapay.com/
  • Отримано реальні ключі API

Далі