Rusty API · v1
Принимайте оплату через СБП с помощью пары запросов
REST API на JSON. Создайте платёж, отправьте покупателя на страницу оплаты и получите webhook, когда деньги придут. Всё остальное, от QR-кода до проверки статуса в банке, мы берём на себя.
15 минут на оплату
Платёж живёт 15 минут, статус обновляется каждые 5 секунд.
Деньги сразу на баланс
После оплаты сумма за вычетом комиссии падает на холд.
Webhook с подписью
HMAC-SHA256 и до 6 повторов, если ваш сервер не ответил.
Безопасно
Ключи храним только в виде отпечатка, все запросы по HTTPS.
Начало
Быстрый старт
Подключение занимает четыре шага. Вам понадобится проверенный магазин в кабинете Rusty.
- 1
Получите API-ключ
Кабинет → Магазины → ваш магазин → вкладка Интеграция → «Выпустить ключ». Ключ показывается один раз, сохраните его в переменных окружения сервера. - 2
Создайте платёж
ОтправьтеPOST /paymentsс суммой и номером заказа. В ответ придутpayment_urlиqr_payload. - 3
Отправьте покупателя на оплату
Перенаправьте его наpayment_url. На телефоне он выберет банк, на компьютере отсканирует QR-код. - 4
Дождитесь webhook
Когда деньги придут, мы отправимpayment.succeededна ваш webhook URL. Проверьте подпись и отметьте заказ оплаченным.
curl -X POST "https://rustypay.pro/api/v1/payments" \
-H "Authorization: Bearer rp_live_ВАШ_КЛЮЧ" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-A-42" \
-d '{
"amount": "2500.50",
"order_id": "A-42",
"title": "Заказ A-42",
"return_url": "https://shop.ru/thanks"
}'Начало
Авторизация
Каждый запрос подписывается секретным ключом магазина в заголовке Authorization. Ключ начинается с rp_live_ и принадлежит одному магазину: платежи, созданные с ним, будут в этом магазине.
Ключ даёт полный доступ к магазину
Храните его только на сервере. Никогда не вставляйте ключ в код сайта, мобильного приложения или в публичный репозиторий. Если ключ утёк, выпустите новый в кабинете: старый перестанет работать сразу.Без ключа или с неверным ключом API ответит 401 unauthorized. Все запросы и ответы в формате JSON в кодировке UTF-8, только по HTTPS.
curl "https://rustypay.pro/api/v1/balance" \
-H "Authorization: Bearer rp_live_ВАШ_КЛЮЧ"Начало
Ошибки
Успешные ответы приходят с кодом 200 или 201. При ошибке API возвращает HTTP-код и объект error с машинным кодом и понятным сообщением на русском.
| HTTP | code | Что случилось |
|---|---|---|
| 400 | invalid_json | Тело запроса не JSON. |
| 401 | unauthorized | Нет ключа или ключ неверный. |
| 403 | account_blocked | Аккаунт заблокирован, обратитесь в поддержку. |
| 404 | not_found | Платёж не найден или принадлежит другому магазину. |
| 422 | invalid_request | Поле заполнено неверно. В message будет имя поля. |
| 422 | invalid_amount | Сумма не число или меньше 0,01 ₽. |
| 422 | payment_failed | Магазин не готов принимать оплату: не проверен, выключен СБП или нет кассы. |
| 502 | payment_failed | Касса временно недоступна. Повторите запрос с тем же Idempotency-Key. |
| 429 | rate_limited | Больше 120 запросов в минуту на ключ. Подождите столько секунд, сколько указано в заголовке Retry-After. |
| 500 | internal_error | Ошибка на нашей стороне. Повторите запрос позже. |
{
"error": {
"code": "invalid_request",
"message": "amount: Введите сумму (например, 1117,32)"
}
}Платежи
Объект платежа
Все методы, которые работают с платежами, и webhook возвращают один и тот же объект. Суммы всегда строки в рублях с двумя знаками после точки.
Поля
idstringобязательныйstatusenumобязательныйpending, paid, expired, canceled или failed. См. «Статусы».amountstringобязательныйfeestringобязательныйnet_amountstringобязательныйcurrencystringобязательныйRUB.titlestringобязательныйorder_idstring | nullнеобязательныйclient_idstring | nullнеобязательныйpayment_urlstringобязательныйqr_payloadstringобязательныйexpires_atISO 8601обязательныйpaid_atISO 8601 | nullнеобязательныйpaid_latebooleanобязательный{
"id": "cmuwwwy3c0003owvizi3zhzqk",
"status": "pending",
"amount": "2500.50",
"fee": "187.54",
"net_amount": "2312.96",
"currency": "RUB",
"title": "Заказ A-42",
"description": null,
"order_id": "A-42",
"client_id": "user_1093",
"payment_url": "https://rustypay.pro/payments/sbp?id=6LC46QpZ...",
"qr_payload": "https://qr.nspk.ru/AD10006M...",
"created_at": "2026-10-06T16:48:02.712Z",
"expires_at": "2026-10-06T17:03:02.711Z",
"paid_at": null,
"paid_late": false
}Платежи
Создать платёж
Создаёт платёж через СБП. Он сразу регистрируется в банке, и у покупателя есть 15 минут, чтобы оплатить. Если время вышло, создайте новый платёж.
Тело запроса
amountstring | numberобязательный"1117.32", "1117,32" или 1117.32. Не больше двух знаков после запятой.titlestringнеобязательныйdescriptionstringнеобязательныйorder_idstringнеобязательныйclient_idstringнеобязательныйreturn_urlstringнеобязательныйЗаголовки
AuthorizationstringобязательныйBearer rp_live_…Idempotency-Keystringнеобязательныйcurl -X POST "https://rustypay.pro/api/v1/payments" \
-H "Authorization: Bearer rp_live_ВАШ_КЛЮЧ" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-A-42" \
-d '{
"amount": "2500.50",
"order_id": "A-42",
"title": "Заказ A-42",
"return_url": "https://shop.ru/thanks"
}'{
"id": "cmuwwwy3c0003owvizi3zhzqk",
"status": "pending",
"amount": "2500.50",
"fee": "187.54",
"net_amount": "2312.96",
"currency": "RUB",
"order_id": "A-42",
"payment_url": "https://rustypay.pro/payments/sbp?id=6LC46QpZ...",
"qr_payload": "https://qr.nspk.ru/AD10006M...",
"expires_at": "2026-10-06T17:03:02.711Z"
}Платежи
Получить платёж
Возвращает текущее состояние платежа. Удобно, если покупатель вернулся на return_url раньше, чем пришёл webhook, или для сверки.
Не опрашивайте слишком часто
Статус в Rusty обновляется раз в 5 секунд. Опрашивать чаще смысла нет, а для фоновой обработки лучше подходит webhook.curl "https://rustypay.pro/api/v1/payments/cmuwwwy3c0003owvizi3zhzqk" \
-H "Authorization: Bearer rp_live_ВАШ_КЛЮЧ"Платежи
Проверить статус платежа
Самый короткий способ узнать, оплачен ли платёж: в ответе только статус и флаг paid, без остальных полей. Удобно на странице «Спасибо за заказ», пока покупатель ждёт, или в фоновой задаче, если вы не используете webhook.
Поля ответа
idstringобязательныйstatusenumобязательныйpending, paid, expired, canceled или failed.paidbooleanобязательныйtrue только для paid.paid_atISO 8601 | nullобязательныйexpires_atISO 8601обязательныйКак часто спрашивать
Раз в 3-5 секунд, пока статусpending. Как только он сменился, опрос можно прекращать: из paid платёж уже никуда не перейдёт.curl "https://rustypay.pro/api/v1/payments/cmuwwwy3c0003owvizi3zhzqk/status" \
-H "Authorization: Bearer rp_live_ВАШ_КЛЮЧ"{
"id": "cmuwwwy3c0003owvizi3zhzqk",
"status": "paid",
"paid": true,
"paid_at": "2026-10-06T17:01:48.000Z",
"expires_at": "2026-10-06T17:03:02.711Z"
}Платежи
Список платежей
Платежи магазина от новых к старым. Подходит для сверки и выгрузки в учётную систему.
Параметры запроса
statusenumнеобязательныйpending, paid, expired, canceled, failed.limitnumberнеобязательныйbeforeISO 8601необязательныйcreated_at последнего платежа.Если has_more равно true, есть ещё платежи: запросите следующую страницу с параметром before.
curl "https://rustypay.pro/api/v1/payments?status=paid&limit=50" \
-H "Authorization: Bearer rp_live_ВАШ_КЛЮЧ"{
"data": [
{ "id": "cmuwx3o2u000mowvicdmgenyb", "status": "paid", "amount": "10.00", ... },
{ "id": "cmuwwwy3c0003owvizi3zhzqk", "status": "expired", "amount": "2500.50", ... }
],
"has_more": true
}Платежи
Статусы платежа
Платёж создаётся в статусе pending и переходит в один из конечных. Из конечного статуса платёж уже не выходит, кроме одного случая ниже.
pending
Ждёт оплаты
| Статус | Значение | Webhook |
|---|---|---|
pending | Ждёт оплаты. Можно оплатить до expires_at. | - |
paid | Оплачен. net_amount зачислен на баланс. | payment.succeeded |
expired | 15 минут прошли, оплаты не было. | payment.expired |
canceled | Банк или касса отклонили платёж. | payment.canceled |
failed | Не удалось создать платёж в кассе. Деньги не списывались. | - |
Оплата после истечения
Иногда банк подтверждает оплату через минуту-другую послеexpires_at. Мы перепроверяем такие платежи ещё час. Если деньги пришли, платёж станет paid с paid_late: true, и вы получите payment.succeeded, даже если до этого был payment.expired.Платежи
Идемпотентность
Сеть ненадёжна: запрос мог дойти, а ответ потеряться. Чтобы при повторе не создать второй платёж, передайте заголовок Idempotency-Key с уникальным значением, например order-A-42.
- 1
Первый запрос
Платёж создаётся, ответ201. - 2
Повтор с тем же ключом
Новый платёж не создаётся, вы получите тот же самый с кодом200.
Ключ: до 64 символов, латиница, цифры и - _ . :. Ключ действует в пределах магазина. Для нового платежа по тому же заказу (например, после истечения) используйте новый ключ: order-A-42-2.
Платежи
Страница оплаты и QR
Проще всего перенаправить покупателя на payment_url. Это наша страница оплаты с таймером, она сама следит за статусом:
- на компьютере показывает QR-код для камеры телефона;
- на телефоне показывает список банков и открывает приложение банка с готовым платежом;
- после оплаты предлагает вернуться на
return_url.
Если хотите встроить оплату в свой интерфейс, нарисуйте QR-код из qr_payload. Это стандартная ссылка СБП, её понимают приложения всех банков. Статус тогда проверяйте через webhook или GET /payments/:id.
import QRCode from "qrcode";
// Свой QR-код вместо нашей страницы оплаты
const svg = await QRCode.toString(payment.qr_payload, {
type: "svg",
errorCorrectionLevel: "M",
});Платежи
Что видит покупатель
Страница по ссылке payment_url сама выбирает экран по статусу платежа и устройству покупателя. Статус на ней обновляется каждые 3 секунды, перезагружать ничего не нужно. Так выглядят все варианты:
Ваш магазин
QR-код
pendingПокупатель открыл ссылку на компьютере. Сканирует код камерой телефона.
Ваш магазин
Выберите банк
Выбор банка
pendingПокупатель на телефоне. Нажимает свой банк, и открывается приложение с готовым платежом.
Ваш магазин
Оплата прошла успешно
Вернуться в магазинОплачено
paidБанк подтвердил оплату. Экран обновляется сам, кнопка ведёт на ваш return_url.
Ваш магазин
Время на оплату истекло
Вернуться в магазинВремя истекло
expiredПрошло 15 минут. Если покупатель уже оплатил, экран сам сменится на «Оплачено».
Ваш магазин
Платёж отменён
Вернуться в магазинПлатёж отменён
canceledБанк или касса отклонили платёж. Покупателю нужно вернуться и создать новый.
Ваш магазин
Платёж недоступен
Недоступен
failedКасса не выдала реквизиты при создании. На практике сюда не попадают: API вернёт ошибку сразу.
Таймер
Показывает, сколько осталось из 15 минут. В последнюю минуту становится красным.Последний банк
На телефоне банк, через который покупатель платил в прошлый раз, показывается первым.Если приложение не открылось
Страница подскажет выбрать другой банк или показать QR-код для другого устройства.Баланс
Получить баланс
Баланс аккаунта, которому принадлежит магазин. Если магазинов несколько, баланс у них общий. Выводить деньги можно в кабинете, в разделе «Выплаты».
Поля ответа
totalstringобязательныйavailablestringобязательныйon_holdstringобязательныйhold_hours часов.pending_payoutsstringобязательныйpaid_outstringобязательныйhold_hoursnumberобязательныйavailable = заработано и прошло холд − выведено − в обработке.curl "https://rustypay.pro/api/v1/balance" \
-H "Authorization: Bearer rp_live_ВАШ_КЛЮЧ"{
"currency": "RUB",
"total": "817537.00",
"available": "790524.00",
"on_hold": "27013.00",
"pending_payouts": "20887.00",
"paid_out": "564120.00",
"hold_hours": 24
}Webhook
События
Укажите webhook URL в кабинете: магазин → Интеграция. Когда статус платежа меняется, мы отправим на этот адрес POST с JSON: тип события и объект платежа.
| Событие | Когда |
|---|---|
payment.succeeded | Платёж оплачен, деньги на балансе. |
payment.expired | Платёж не оплатили за 15 минут. |
payment.canceled | Банк или касса отклонили платёж. |
test | Тестовое событие из кабинета, проверка вашего обработчика. |
Заголовки запроса
X-Rusty-EventstringобязательныйX-Rusty-Signaturestringобязательныйsha256=<hex>.User-AgentstringобязательныйRustyPay-Webhooks/1.0POST /webhooks/rusty HTTP/1.1
Content-Type: application/json
User-Agent: RustyPay-Webhooks/1.0
X-Rusty-Event: payment.succeeded
X-Rusty-Signature: sha256=5d41402abc4b2a76b9719d911017c592...
{
"event": "payment.succeeded",
"payment": {
"id": "cmuwx3o2u000mowvicdmgenyb",
"status": "paid",
"amount": "10.00",
"net_amount": "9.20",
"order_id": "A-42",
"paid_at": "2026-10-06T17:01:48.000Z",
...
}
}Webhook
Проверка подписи
Любой может отправить запрос на ваш webhook URL, поэтому всегда проверяйте подпись, прежде чем отмечать заказ оплаченным.
- 1
Возьмите секрет
Он в кабинете рядом с webhook URL, начинается сwhsec_. - 2
Посчитайте HMAC
HMAC-SHA256 от сырого тела запроса, ключ = секрет, результат в hex. - 3
Сравните
Строкаsha256=+ hex должна совпасть с заголовкомX-Rusty-Signature. Сравнивайте функцией с постоянным временем:timingSafeEqual,hmac.compare_digest,hash_equals.
Считайте подпись до разбора JSON
Если сначала распарсить тело, а потом снова превратить в строку, порядок полей или пробелы могут измениться, и подпись не сойдётся.import crypto from "crypto";
import express from "express";
const app = express();
// Подпись считается от сырого тела, поэтому не парсите JSON заранее
app.post("/webhooks/rusty", express.raw({ type: "application/json" }), (req, res) => {
const expected = "sha256=" + crypto
.createHmac("sha256", process.env.RUSTY_WEBHOOK_SECRET)
.update(req.body)
.digest("hex");
const received = req.get("X-Rusty-Signature") ?? "";
const valid = received.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected));
if (!valid) return res.sendStatus(401);
const { event, payment } = JSON.parse(req.body);
if (event === "payment.succeeded") {
markOrderPaid(payment.order_id, payment.id); // должно быть идемпотентным
}
res.sendStatus(200);
});Webhook
Повторы и тесты
Ответьте любым кодом 2xx в течение 10 секунд. Если сервер не ответил, вернул ошибку или перенаправление, мы повторим отправку:
После шестой неудачной попытки событие помечается недоставленным. Статус платежа всегда можно узнать через GET /payments/:id.
Обрабатывайте события идемпотентно
Одно и то же событие может прийти дважды, например если ваш ответ потерялся в сети. Запоминайтеpayment.id и не начисляйте заказ повторно.Как проверить обработчик
В кабинете, во вкладке «Интеграция», нажмите «Отправить тестовое событие». Придёт подписанный запрос с событиемtest, а в кабинете вы увидите HTTP-код и время ответа вашего сервера.Готовы подключиться?
Выпустите ключ и создайте первый платёж за пару минут
