Перейти к основному содержимому

Колбэки (вебхуки)

RollyPay отправляет на ваш сервер HTTP POST при наступлении событий: оплата, отмена, истечение срока платежа, возврат и т.д. Колбэк приходит и по успешным, и по неуспешным исходам — см. Неуспешные платежи.

Настройка

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Валюта платежа
testtrue для платежей в тестовом режиме (sandbox), иначе false
metadataПередаётся только если метаданные были заданы при создании платежа

Проверка подписи

В каждом вебхуке передаются два заголовка:

ЗаголовокОписание
X-SignatureHMAC-SHA256 подпись
X-TimestampUnix-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))
}

Типы событий

СобытиеКогда приходитstatus в теле
payment.createdПлатёж созданcreated
payment.paidОплата успешно подтвержденаpaid
payment.canceledПлатёж отменён или отклонён — деньги не полученыcanceled
payment.expiredИстёк срок жизни платежа, оплата так и не поступилаexpired
payment.chargebackЧарджбек по оплаченному платежуchargeback
payment.refundedПо платежу выполнен возвратrefunded
payout.completedВывод успешно выполнен
refund_request.completedЗаявка на возврат завершена; отправляется вместе с payment.refunded (сначала это событие, затем payment.refunded) — оба колбэка несут тот же платёж и status: refunded, достаточно обработать одно из них, если вы не разделяете логику по event_typerefunded

Неуспешные платежи

Отдельного события payment.failed не существует. Платёж, по которому деньги не получены, заканчивается одним из двух исходов:

СтатусСобытиеЧто произошло
canceledpayment.canceledПлатёж отменён или отклонён — например, отказ на стороне банка или провайдера, отмена покупателем
expiredpayment.expiredСрок жизни платежа вышел, оплата не поступила

По каждому из этих исходов колбэк отправляется — при условии, что на кассе задан рабочий callback_url. Раньше неоплаченный платёж мог остаться без уведомления; теперь и отмена, и истечение срока приводят к колбэку. Заказ не нужно держать в состоянии «ждём оплату» до бесконечности: дождитесь одного из финальных событий и закройте его.

Ориентируйтесь на поле status — это состояние платежа, а event_type вычисляется из него. Если вы уже разбираете колбэки по status, новое событие не потребует изменений в коде.

Оба исхода финальные. Единственное исключение — поздняя оплата: если деньги дошли уже после истечения срока, платёж может перейти из expired в paid, и вы получите ещё один колбэк с payment.paid. Поэтому обработчик должен опираться на последнее полученное событие по payment_id, а не только на первое.

Пример: истёкший платёж

{
"event_type": "payment.expired",
"payment_id": "pay_a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"order_id": "order_12345",
"status": "expired",
"amount": "1500.00",
"currency": "RUB",
"test": false
}

Тестовые колбэки

Колбэки по тестовым (sandbox) платежам приходят в том же формате и с той же подписью, что и боевые. Отличает их поле "test": true в теле и заголовок X-Test-Mode: true. См. Тестовый режим (Sandbox).

Повторные попытки

Если ваш endpoint вернул не 2xx или не ответил в течение 10 секунд, RollyPay повторяет запрос с экспоненциальной задержкой:

Неудачная попыткаСледующая попытка через
130 сек
21 мин
32 мин
44 мин
58 мин
616 мин
732 мин
8повторов больше нет, доставка помечается как неуспешная

Формула задержки: 30 сек × 2^(номер неудачной попытки − 1). Всего попыток не более 8, последняя — примерно через час после первой. После успешного 2xx повторная отправка этого события не выполняется.