Rusty API · v1

Принимайте оплату через СБП с помощью пары запросов

REST API на JSON. Создайте платёж, отправьте покупателя на страницу оплаты и получите webhook, когда деньги придут. Всё остальное, от QR-кода до проверки статуса в банке, мы берём на себя.

Базовый URLhttps://rustypay.pro/api/v1
rustypay.pro/api/v1Запрос

15 минут на оплату

Платёж живёт 15 минут, статус обновляется каждые 5 секунд.

Деньги сразу на баланс

После оплаты сумма за вычетом комиссии падает на холд.

Webhook с подписью

HMAC-SHA256 и до 6 повторов, если ваш сервер не ответил.

Безопасно

Ключи храним только в виде отпечатка, все запросы по HTTPS.

Начало

Быстрый старт

Подключение занимает четыре шага. Вам понадобится проверенный магазин в кабинете Rusty.

  1. 1

    Получите API-ключ

    Кабинет → Магазины → ваш магазин → вкладка Интеграция → «Выпустить ключ». Ключ показывается один раз, сохраните его в переменных окружения сервера.
  2. 2

    Создайте платёж

    Отправьте POST /payments с суммой и номером заказа. В ответ придут payment_url и qr_payload.
  3. 3

    Отправьте покупателя на оплату

    Перенаправьте его на payment_url. На телефоне он выберет банк, на компьютере отсканирует QR-код.
  4. 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_ и принадлежит одному магазину: платежи, созданные с ним, будут в этом магазине.

Authorization: Bearer rp_live_xxxxxxxxxxxxxxxx

Ключ даёт полный доступ к магазину

Храните его только на сервере. Никогда не вставляйте ключ в код сайта, мобильного приложения или в публичный репозиторий. Если ключ утёк, выпустите новый в кабинете: старый перестанет работать сразу.

Без ключа или с неверным ключом API ответит 401 unauthorized. Все запросы и ответы в формате JSON в кодировке UTF-8, только по HTTPS.

Запрос с ключом
curl "https://rustypay.pro/api/v1/balance" \
  -H "Authorization: Bearer rp_live_ВАШ_КЛЮЧ"

Начало

Ошибки

Успешные ответы приходят с кодом 200 или 201. При ошибке API возвращает HTTP-код и объект error с машинным кодом и понятным сообщением на русском.

HTTPcodeЧто случилось
400invalid_jsonТело запроса не JSON.
401unauthorizedНет ключа или ключ неверный.
403account_blockedАккаунт заблокирован, обратитесь в поддержку.
404not_foundПлатёж не найден или принадлежит другому магазину.
422invalid_requestПоле заполнено неверно. В message будет имя поля.
422invalid_amountСумма не число или меньше 0,01 ₽.
422payment_failedМагазин не готов принимать оплату: не проверен, выключен СБП или нет кассы.
502payment_failedКасса временно недоступна. Повторите запрос с тем же Idempotency-Key.
429rate_limitedБольше 120 запросов в минуту на ключ. Подождите столько секунд, сколько указано в заголовке Retry-After.
500internal_errorОшибка на нашей стороне. Повторите запрос позже.
Ответ с ошибкой
{
  "error": {
    "code": "invalid_request",
    "message": "amount: Введите сумму (например, 1117,32)"
  }
}

Платежи

Объект платежа

Все методы, которые работают с платежами, и webhook возвращают один и тот же объект. Суммы всегда строки в рублях с двумя знаками после точки.

Поля

idstringобязательный
Идентификатор платежа в Rusty.
statusenumобязательный
Статус: pending, paid, expired, canceled или failed. См. «Статусы».
amountstringобязательный
Сумма, которую заплатит покупатель.
feestringобязательный
Комиссия Rusty с этого платежа.
net_amountstringобязательный
Сколько придёт на ваш баланс: amount минус fee.
currencystringобязательный
Всегда RUB.
titlestringобязательный
Название платежа, его видит покупатель.
order_idstring | nullнеобязательный
Номер заказа в вашей системе.
client_idstring | nullнеобязательный
Идентификатор покупателя в вашей системе.
payment_urlstringобязательный
Страница оплаты для покупателя.
qr_payloadstringобязательный
Ссылка СБП (qr.nspk.ru), если рисуете QR сами.
expires_atISO 8601обязательный
До какого момента платёж можно оплатить.
paid_atISO 8601 | nullнеобязательный
Когда платёж оплачен.
paid_latebooleanобязательный
Банк подтвердил оплату уже после expires_at. Деньги всё равно зачислены.
payment
{
  "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
}

Платежи

Создать платёж

POST/api/v1/payments

Создаёт платёж через СБП. Он сразу регистрируется в банке, и у покупателя есть 15 минут, чтобы оплатить. Если время вышло, создайте новый платёж.

Тело запроса

amountstring | numberобязательный
Сумма в рублях: "1117.32", "1117,32" или 1117.32. Не больше двух знаков после запятой.
titlestringнеобязательный
Название, до 120 символов. Если не указать, будет «Заказ {order_id}».
descriptionstringнеобязательный
Описание для покупателя, до 500 символов.
order_idstringнеобязательный
Номер заказа в вашей системе, до 100 символов. Вернётся в ответе и в webhook.
client_idstringнеобязательный
Идентификатор покупателя, до 100 символов.
return_urlstringнеобязательный
Куда вернуть покупателя после оплаты. Только http:// или https://.

Заголовки

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"
  }'
Ответ201
{
  "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"
}

Платежи

Получить платёж

GET/api/v1/payments/:id

Возвращает текущее состояние платежа. Удобно, если покупатель вернулся на return_url раньше, чем пришёл webhook, или для сверки.

Не опрашивайте слишком часто

Статус в Rusty обновляется раз в 5 секунд. Опрашивать чаще смысла нет, а для фоновой обработки лучше подходит webhook.
Запрос
curl "https://rustypay.pro/api/v1/payments/cmuwwwy3c0003owvizi3zhzqk" \
  -H "Authorization: Bearer rp_live_ВАШ_КЛЮЧ"

Платежи

Проверить статус платежа

GET/api/v1/payments/:id/status

Самый короткий способ узнать, оплачен ли платёж: в ответе только статус и флаг 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_ВАШ_КЛЮЧ"
Ответ200
{
  "id": "cmuwwwy3c0003owvizi3zhzqk",
  "status": "paid",
  "paid": true,
  "paid_at": "2026-10-06T17:01:48.000Z",
  "expires_at": "2026-10-06T17:03:02.711Z"
}

Платежи

Список платежей

GET/api/v1/payments

Платежи магазина от новых к старым. Подходит для сверки и выгрузки в учётную систему.

Параметры запроса

statusenumнеобязательный
Только платежи в этом статусе: pending, paid, expired, canceled, failed.
limitnumberнеобязательный
Сколько вернуть, от 1 до 100. По умолчанию 20.
beforeISO 8601необязательный
Только платежи, созданные раньше этого момента. Для следующей страницы передайте created_at последнего платежа.

Если has_more равно true, есть ещё платежи: запросите следующую страницу с параметром before.

Запрос
curl "https://rustypay.pro/api/v1/payments?status=paid&limit=50" \
  -H "Authorization: Bearer rp_live_ВАШ_КЛЮЧ"
Ответ200
{
  "data": [
    { "id": "cmuwx3o2u000mowvicdmgenyb", "status": "paid", "amount": "10.00", ... },
    { "id": "cmuwwwy3c0003owvizi3zhzqk", "status": "expired", "amount": "2500.50", ... }
  ],
  "has_more": true
}

Платежи

Статусы платежа

Платёж создаётся в статусе pending и переходит в один из конечных. Из конечного статуса платёж уже не выходит, кроме одного случая ниже.

pending

Ждёт оплаты

↓
paidДеньги на балансе
expiredНе оплачен за 15 минут
canceledОтклонён банком или кассой
СтатусЗначениеWebhook
pendingЖдёт оплаты. Можно оплатить до expires_at.-
paidОплачен. net_amount зачислен на баланс.payment.succeeded
expired15 минут прошли, оплаты не было.payment.expired
canceledБанк или касса отклонили платёж.payment.canceled
failedНе удалось создать платёж в кассе. Деньги не списывались.-

Оплата после истечения

Иногда банк подтверждает оплату через минуту-другую после expires_at. Мы перепроверяем такие платежи ещё час. Если деньги пришли, платёж станет paid с paid_late: true, и вы получите payment.succeeded, даже если до этого был payment.expired.

Платежи

Идемпотентность

Сеть ненадёжна: запрос мог дойти, а ответ потеряться. Чтобы при повторе не создать второй платёж, передайте заголовок Idempotency-Key с уникальным значением, например order-A-42.

  1. 1

    Первый запрос

    Платёж создаётся, ответ 201.
  2. 2

    Повтор с тем же ключом

    Новый платёж не создаётся, вы получите тот же самый с кодом 200.

Ключ: до 64 символов, латиница, цифры и - _ . :. Ключ действует в пределах магазина. Для нового платежа по тому же заказу (например, после истечения) используйте новый ключ: order-A-42-2.

Платежи

Страница оплаты и QR

Проще всего перенаправить покупателя на payment_url. Это наша страница оплаты с таймером, она сама следит за статусом:

  • на компьютере показывает QR-код для камеры телефона;
  • на телефоне показывает список банков и открывает приложение банка с готовым платежом;
  • после оплаты предлагает вернуться на return_url.

Если хотите встроить оплату в свой интерфейс, нарисуйте QR-код из qr_payload. Это стандартная ссылка СБП, её понимают приложения всех банков. Статус тогда проверяйте через webhook или GET /payments/:id.

Свой QR-код
import QRCode from "qrcode";

// Свой QR-код вместо нашей страницы оплаты
const svg = await QRCode.toString(payment.qr_payload, {
  type: "svg",
  errorCorrectionLevel: "M",
});

Платежи

Что видит покупатель

Страница по ссылке payment_url сама выбирает экран по статусу платежа и устройству покупателя. Статус на ней обновляется каждые 3 секунды, перезагружать ничего не нужно. Так выглядят все варианты:

Rusty Безопасная оплата

Ваш магазин

1 117,32 ₽ 14:52
1Откройте камеру2Наведите на QR3Подтвердите

QR-код

pending

Покупатель открыл ссылку на компьютере. Сканирует код камерой телефона.

Rusty Безопасная оплата

Ваш магазин

1 117,32 ₽ 14:52

Выберите банк

Найти банк
Сбербанк
Т-Банк
ВТБ
Альфа-Банк
Райффайзен

Выбор банка

pending

Покупатель на телефоне. Нажимает свой банк, и открывается приложение с готовым платежом.

Rusty Безопасная оплата

Ваш магазин

1 117,32 ₽

Оплата прошла успешно

Вернуться в магазин

Оплачено

paid

Банк подтвердил оплату. Экран обновляется сам, кнопка ведёт на ваш return_url.

Rusty Безопасная оплата

Ваш магазин

1 117,32 ₽

Время на оплату истекло

Вернуться в магазин

Время истекло

expired

Прошло 15 минут. Если покупатель уже оплатил, экран сам сменится на «Оплачено».

Rusty Безопасная оплата

Ваш магазин

1 117,32 ₽

Платёж отменён

Вернуться в магазин

Платёж отменён

canceled

Банк или касса отклонили платёж. Покупателю нужно вернуться и создать новый.

Rusty Безопасная оплата

Ваш магазин

1 117,32 ₽

Платёж недоступен

Недоступен

failed

Касса не выдала реквизиты при создании. На практике сюда не попадают: API вернёт ошибку сразу.

Таймер

Показывает, сколько осталось из 15 минут. В последнюю минуту становится красным.

Последний банк

На телефоне банк, через который покупатель платил в прошлый раз, показывается первым.

Если приложение не открылось

Страница подскажет выбрать другой банк или показать QR-код для другого устройства.

Баланс

Получить баланс

GET/api/v1/balance

Баланс аккаунта, которому принадлежит магазин. Если магазинов несколько, баланс у них общий. Выводить деньги можно в кабинете, в разделе «Выплаты».

Поля ответа

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_ВАШ_КЛЮЧ"
Ответ200
{
  "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обязательный
Тип события, то же, что в поле event.
X-Rusty-Signaturestringобязательный
Подпись тела запроса: sha256=<hex>.
User-Agentstringобязательный
RustyPay-Webhooks/1.0
Что придёт на ваш сервер
POST /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. 1

    Возьмите секрет

    Он в кабинете рядом с webhook URL, начинается с whsec_.
  2. 2

    Посчитайте HMAC

    HMAC-SHA256 от сырого тела запроса, ключ = секрет, результат в hex.
  3. 3

    Сравните

    Строка sha256= + hex должна совпасть с заголовком X-Rusty-Signature. Сравнивайте функцией с постоянным временем: timingSafeEqual, hmac.compare_digest, hash_equals.

Считайте подпись до разбора JSON

Если сначала распарсить тело, а потом снова превратить в строку, порядок полей или пробелы могут измениться, и подпись не сойдётся.
Обработчик webhook
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 секунд. Если сервер не ответил, вернул ошибку или перенаправление, мы повторим отправку:

#1сразу
#230 сек
#32 мин
#410 мин
#530 мин
#62 ч

После шестой неудачной попытки событие помечается недоставленным. Статус платежа всегда можно узнать через GET /payments/:id.

Обрабатывайте события идемпотентно

Одно и то же событие может прийти дважды, например если ваш ответ потерялся в сети. Запоминайте payment.id и не начисляйте заказ повторно.

Как проверить обработчик

В кабинете, во вкладке «Интеграция», нажмите «Отправить тестовое событие». Придёт подписанный запрос с событием test, а в кабинете вы увидите HTTP-код и время ответа вашего сервера.

Готовы подключиться?

Выпустите ключ и создайте первый платёж за пару минут