Колбэки (вебхуки)
RollyPay отправляет на ваш сервер HTTP POST при наступлении событий: оплата, отмена, истечение срока платежа, возврат и т.д. Колбэк приходит и по успешным, и по неуспешным исходам — см. Неуспешные платежи.
Зачем нужна интеграция
Интеграция по колбэкам — не опциональное удобство, а обязательное условие корректной работы кассы. Платёжный цикл асинхронный: подтверждение оплаты, отказ банка, истечение срока, а также поздние возвраты и отмены приходят не в ответе на ваш запрос, а отдельными событиями во времени. Единственный канал, по которому платформа доводит до вас эти изменения состояния, — колбэк на ваш callback_url.
Без рабочего обработчика касса продолжит принимать платежи, но ваша сторона не узнает об их финальном статусе: заказы зависнут в ожидании оплаты, поздно пришедшие или отозванные средства останутся неучтёнными, а расхождения между вашим учётом и балансом кассы придётся выверять вручную. Поэтому endpoint для колбэков должен быть развёрнут, доступен и проверять подпись до того, как касса выйдет в боевой режим.
Отдельно обратите внимание на асинхронные банковские исходы — возвраты и о тмены. Банк-эмитент может сообщить об отзыве или возврате средств уже после того, как платёж считался завершённым. Такие уведомления платформа принимает и ретранслирует вам колбэком в рамках допустимого окна — ограниченного периода после исходной операции, в течение которого поздний исход ещё относится к этому платежу (см. Поздние банковские исходы). Если ваша сторона не обрабатывает эти события, возврат или отмена, инициированные банком, пройдут мимо вашего учёта.
Настройка
URL для колбэков задаётся в настройках кассы (callback_url). Его можно задать при создании кассы или через API:
PUT /api/v1/terminals/{terminalID}
{
"callback_url": "https://your-server.com/webhooks/rollypay"
}
Сервер должен отвечать кодом 2xx в течение 10 секунд. Иначе запрос будет повторён по политике retry.
Тело запроса
Каждый колбэк — JSON, например:
{
"event_type": "payment.paid",
"payment_id": "pay_a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"order_id": "order_12345",
"status": "paid",
"amount": "1500.00",
"currency": "RUB",
"test": false
}
| Поле | Описание |
|---|---|
event_type | Тип события (см. ниже). Всегда соответствует полю status |
payment_id | Идентификатор платежа в RollyPay |
order_id | Ваш идентификатор заказа, переданный при создании платежа |
status | Текущий статус платежа |
amount | Сумма платежа, строка с двумя знаками после запятой |
currency | Валюта платежа |
test | true для платежей в тестовом режиме (sandbox), иначе false |
metadata | Передаётся только если метаданные были заданы при создании платежа |
Проверка подписи
В каждом вебхуке передаются два заголовка:
| Заголовок | Описание |
|---|---|
X-Signature | HMAC-SHA256 подпись |
X-Timestamp | Unix-timestamp момента отправки |
Подпись вычисляется от строки timestamp + "." + body с ключом signing_secret кассы. Проверять подпись обязательно перед обработкой.
JavaScript (Node)
const crypto = require("crypto");
function verifySignature(body, timestamp, signature, signingSecret) {
const expected = crypto
.createHmac("sha256", signingSecret)
.update(timestamp + "." + body)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signature)
);
}
app.post("/webhooks/rollypay", (req, res) => {
const signature = req.headers["x-signature"];
const timestamp = req.headers["x-timestamp"];
const rawBody = req.rawBody; // нужен именно сырой body, не распарсенный
if (!verifySignature(rawBody, timestamp, signature, process.env.SIGNING_SECRET)) {
return res.status(403).send("Invalid signature");
}
const event = JSON.parse(rawBody);
// Обработка event...
res.status(200).send("OK");
});
Go
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
)
func verifySignature(body []byte, timestamp, signature, signingSecret string) bool {
mac := hmac.New(sha256.New, []byte(signingSecret))
mac.Write([]byte(timestamp))
mac.Write([]byte("."))
mac.Write(body)
expected := hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(expected), []byte(signature))
}