API приёма платежей
Публичная документация для клиентов (интеграторов): как устроен обмен, кто кому что отправляет, статусы заказа и вебхуки. Эта страница и её LLM-версия доступны без авторизации.
1 Общая картина: участники и потоки #
В обмене участвуют четверо. Вы (клиент/интегратор) создаёте заказы и принимаете вебхуки. orders-api — сервер API этого стенда. СБП — даёт QR/ссылку на оплату и сообщает об оплате. Плательщик — ваш пользователь, который сканирует QR в приложении банка.
flowchart LR
Payer["👤 Плательщик
(приложение банка)"]
Client["🧑💻 Ваш сервер
(интегратор)"]
Order["⚙️ orders-api
этот стенд"]
Provider["🏦 СБП"]
WH["📬 Ваш webhook_url"]
Client -- "POST /api/v1/orders
Bearer-токен" --> Order
Order -- "создание платёжной сессии
подпись RSA" --> Provider
Provider -- "сессия + QR + deeplink" --> Order
Order -- "201: order_id, QR,
sbp_deeplink" --> Client
Payer -- "сканирует QR,
подтверждает оплату" --> Provider
Provider -- "вебхук
payment_finished" --> Order
Order -- "200 OK (всегда)" --> Provider
Order -- "order.status_changed
X-Webhook-Signature (RSA)" --> WH
WH -- "2xx" --> Order
style Order fill:var(--accent-soft),stroke:var(--link)
style Provider fill:var(--warn-bg),stroke:var(--warn-text)
style Client fill:var(--ok-bg),stroke:var(--ok-text)
style Payer fill:var(--th-bg),stroke:var(--muted)
style WH fill:var(--ok-bg),stroke:var(--ok-text)
Кто что вызывает
POST/GET /api/v1/orders… — создание, списки, действияpayment_finished и др.POST <webhook_url> — order.status_changed при каждой реальной смене статусаЧто приходит вам
POST /api/v1/orders201order.status_changedorder.status_changedorder.status_changed202Вебхуки приходят только если вашему клиенту задан webhook_url (задаёт оператор при заведении клиента).
2 Жизненный цикл заказа #
Полная последовательность от создания до успешной оплаты:
sequenceDiagram
autonumber
participant C as Ваш сервер
participant O as orders-api
participant P as СБП
participant U as Плательщик
C->>O: POST /api/v1/orders (amount USD, return_url, …)
Note over O: пауза? лимиты? курс из кеша?
rub_amount = USD × курс × (1+markup)
O->>P: создание платёжной сессии (подпись RSA)
P-->>O: сессия + sbp_deeplink + QR
O-->>C: 201 { order_id, status, rub_amount, QR }
O->>C: вебхук order.status_changed (processing)
U->>P: сканирует QR, подтверждает в банке
P->>O: вебхук (payment_finished, accepted)
O-->>P: 200 OK
O->>C: вебхук order.status_changed (succeeded)
Note over C: заказ оплачен — деньги зачислены
opt Отмена до оплаты
C->>O: POST /api/v1/orders/{id}/cancel
O->>P: отмена сессии
O-->>C: 200 { status: canceled }
O->>C: вебхук order.status_changed (canceled)
end
opt Возврат после оплаты
C->>O: POST /api/v1/orders/{id}/refund { amount RUB | полный }
O->>P: возврат платежа
O-->>C: 200 OK (или 202 pending_review — модерация)
O->>C: вебхук (refunded / partially_refunded)
end
Что сервис проверяет при создании (по порядку)
- Токен — клиент существует, активен, секрет совпал (
401). - Пауза — оператор мог приостановить создание заказов (
503). - Лимиты клиента — min/max суммы, дневные/недельные обороты и число заказов (
400). - Курс — свежий USD/RUB в кеше; нет —
503, заказ не создаётся. - Идемпотентность — повтор с тем же ключом вернёт существующий заказ.
- СБП — система недоступна: заказ сохраняется со статусом
failed, ответ502, вебхук клиенту отправляется.
3 Диаграмма статусов #
stateDiagram-v2
[*] --> pending : POST /orders
pending --> created : сессия СБП создана
pending --> failed : СБП недоступна (502)
created --> processing : СБП начала обработку
created --> canceled : cancel / срок сессии
processing --> action_required : ждёт действия плательщика
processing --> awaiting_confirm
processing --> awaiting_capture
processing --> succeeded : оплачен (accepted)
processing --> canceled
processing --> failed
action_required --> succeeded
action_required --> canceled
awaiting_confirm --> succeeded : confirm
awaiting_capture --> succeeded
succeeded --> partially_refunded : частичный возврат
succeeded --> refunded : полный возврат
partially_refunded --> refunded : возврат остатка
succeeded : succeeded ✅ финал*
canceled : canceled ⛔ финал
failed : failed ⛔ финал
refunded : refunded ↩ финал
* succeeded финален до возврата: refunded/partially_refunded — тоже финальные.
Финальные статусы «липкие»: запоздавший вебхук с промежуточным статусом их не затирает.
Статусы и их источники
| Статус | Значение | Кто выставляет |
|---|---|---|
| pending | запись создана, запрос в СБП ещё не ушёл/идёт | сервис при создании |
| created | платёжная сессия создана, QR выдан | ответ СБП / вебхук СБП |
| processing | СБП обрабатывает платёж (in_progress) | вебхук СБП / sync |
| action_required | нужно действие плательщика (next_action) | вебхук СБП |
| awaiting_confirm | операция ждёт confirm | вебхук СБП |
| awaiting_capture | операция ждёт capture | вебхук СБП |
| succeeded | оплачен (сессия accepted) | вебхук СБП / confirm / sync |
| canceled | отменён до оплаты | cancel / вебхук СБП |
| failed | ошибка (в т.ч. недоступность СБП) | сервис / вебхук СБП |
| refunded | возвращена вся сумма | по сумме принятых возвратов СБП |
| partially_refunded | возвращена часть суммы | по сумме принятых возвратов СБП |
accepted —
возвраты видны только в списке возвратов платежа внутри сессии. Сервис сам пересчитывает их
сумму и выставляет refunded/partially_refunded — вам об этом приходит вебхук.4 Основы #
- База:
https://fdsklfgejhmgrfdgbdfy.site, всё — JSON (кодировка UTF-8). - Заказы создаются в USD в центах (
10000= $100.00), оплата идёт в рублях через СБП. - Сумма в рублях считается сервисом: курс USD/RUB из кеша × (1 + наценка клиента). Итог приходит в ответе (
rub_amount, копейки) и фиксируется в заказе. - Ссылка на оплату (СБП deeplink) и QR — в ответе создания заказа и в вебхуках.
- Длина тела запроса — до 1 МиБ.
- Время в ответах — UTC (ISO 8601). Даты в фильтрах — по МСК.
5 Авторизация #
Bearer-токен в заголовке. Формат: <client_uuid>.<secret> — выдаётся при заведении
вашего клиента, у каждого клиента свой ключ. Перевыпуск токена делает старый недействительным.
Для удобства все примеры ниже используют переменные окружения — задайте их один раз:
export TOKEN="<client_uuid>.<secret>"
export API="https://fdsklfgejhmgrfdgbdfy.site"
# проверка: список ваших заказов
curl -s "$API/api/v1/orders?limit=1" -H "Authorization: Bearer $TOKEN" | jqОшибки: 401 — токен отсутствует/неверен/клиент на паузе.
Изоляция: вы видите и двигаете только свои заказы — чужой id даст 403.
6 Создание заказа #
POST /api/v1/orders
| Поле | Тип | Описание |
|---|---|---|
amount | int, обязательное | Сумма в центах USD, > 0 |
currency | string | Только usd (можно не указывать) |
return_url | string, обязательное | Куда вернуть плательщика |
customer_ref | string | Ваш идентификатор плательщика (иначе — idempotency_key или случайный UUID) |
payment_description | string | Назначение платежа |
sender_ipv4 | string | IPv4 плательщика (валидируется) |
idempotency_key | string | Ключ идемпотентности (см. ниже) |
1Создайте заказ и сохраните его id
ORDER_ID=$(curl -s -X POST "$API/api/v1/orders" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"amount": 100,
"currency": "usd",
"return_url": "https://shop.example.com/return",
"customer_ref": "user-42",
"payment_description": "Заказ №1042",
"idempotency_key": "order-1042"
}' | jq -r .order_id)
echo "$ORDER_ID"Ответ 201:
{
"order_id": "7c9e…",
"status": "processing",
"amount": 100,
"currency": "usd",
"rate_used": 87.46, // курс, зафиксированный в заказе
"rub_amount": 8746, // сумма к оплате: копейки RUB
"return_url": "https://shop.example.com/return",
"sbp_deeplink": "https://qr.nspk.ru/…", // ссылка на оплату
"qr_image_base64": "…", // картинка QR
"created_at": "…", "updated_at": "…"
}2Отдайте плательщику ссылку или QR
Поле sbp_deeplink — ссылка на оплату (СБП), qr_image_base64 — картинка QR.
На demo-стенде «оплату» можно сымитировать действием confirm (см. раздел 9).
3Проверьте результат
curl -s "$API/api/v1/orders/$ORDER_ID" \
-H "Authorization: Bearer $TOKEN" | jq '{status, rub_amount}'Коды ошибок: 400 — валидация/лимиты; 503 — нет свежего курса или
создание заказов приостановлено оператором; 502 — СБП недоступна
(заказ при этом сохраняется со статусом failed).
7 Список заказов #
GET /api/v1/orders — только ваши заказы.
Query-параметры: id (префикс UUID),
status, amount_min/amount_max (USD),
from/to (YYYY-MM-DD, по МСК), limit (≤500).
curl -s "$API/api/v1/orders?status=succeeded&from=2026-08-01&to=2026-08-15&limit=20" \
-H "Authorization: Bearer $TOKEN" | jq 'length'curl -s "$API/api/v1/orders?id=7c9e" \
-H "Authorization: Bearer $TOKEN" | jq '.[].order_id'8 Карточка заказа #
GET /api/v1/orders/{id} — публичная карточка заказа:
статус, суммы, зафиксированный курс, QR/ссылка на оплату. Чужой заказ → 403.
Данные платёжного провайдера (его идентификаторы, статусы, логи) клиенту не отдаются.
curl -s "$API/api/v1/orders/$ORDER_ID" \
-H "Authorization: Bearer $TOKEN" | jq9 Действия по заказу #
POST /api/v1/orders/{id}/{action} — только свои заказы.
Каждое действие, меняющее статус, порождает вебхук order.status_changed.
| Action | Тело | Описание | Ответ |
|---|---|---|---|
confirm | — | Подтвердить операцию (после готовности СБП) | 200, статус из СБП |
cancel | — | Отменить неоплаченный заказ | 200, canceled |
refund | {"amount": <RUB копейки>, "reason": "…"} или {} | Возврат: полный или частичный (не больше списанного), с необязательной причиной. При модерации возвратов — заявка в пул | 200 или 202 pending_review |
demo-confirm | — | Demo: «оплатить» заказ в тестовом режиме (может отвечать 503 — просто повторите) | 200, succeeded |
demo-cancel | — | Demo: отменить заказ в тестовом режиме | 200, canceled |
curl -s -X POST "$API/api/v1/orders/$ORDER_ID/confirm" \
-H "Authorization: Bearer $TOKEN"curl -s -X POST "$API/api/v1/orders/$ORDER_ID/cancel" \
-H "Authorization: Bearer $TOKEN"curl -s -X POST "$API/api/v1/orders/$ORDER_ID/refund" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"reason": "покупатель отказался от заказа"}'curl -s -X POST "$API/api/v1/orders/$ORDER_ID/refund" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"amount": 5000, "reason": "частичная компенсация"}'10 Вебхуки клиенту #
Если вашему клиенту задан webhook_url, при каждой реальной смене статуса
заказа сервис присылает POST. Триггеры: создание заказа, вебхуки из СБП,
ваши действия confirm/cancel/refund/demo-*, синхронизация статуса.
flowchart LR
subgraph Триггеры
A1[создание заказа]
A2[вебхук из СБП]
A3[confirm / cancel / refund]
A4[sync статуса]
end
B{"статус реально
изменился?"}
C["POST webhook_url
order.status_changed"]
D["ответ 2xx → done"]
E["таймаут/5xx/429 →
до 3 попыток с бэкоффом"]
F["прочие 4xx →
без ретрая"]
A1 --> B
A2 --> B
A3 --> B
A4 --> B
B -- "да" --> C
B -- "нет (дубль)" --> X[ничего не отправляется]
C --> D
C --> E
C --> F
style C fill:var(--accent-soft),stroke:var(--link)
style X fill:var(--th-bg),stroke:var(--muted)
Тело вебхука:
{
"type": "order.status_changed",
"order": {
"order_id": "…", "status": "processing",
"amount": 100, "currency": "usd",
"rub_amount": 8746, "rate_used": 87.46,
"return_url": "…",
"created_at": "…", "updated_at": "…"
},
"payment": { // пока оплата актуальна
"sbp_deeplink": "https://qr.nspk.ru/…",
"qr_image_base64": "…",
"redirect_url": ""
},
"timestamp": "…"
}Заголовки: X-Webhook-Event: order.status_changed и
X-Webhook-Signature — подпись Base64(RSA-SHA256(body)).
Проверяйте её публичным ключом, который вам выдали. Отвечайте 2xx;
при таймауте/5xx/429 доставка повторится до 3 раз с нарастающей паузой, затем
продолжится фоново каждые 5 минут (не более 20 доставок на заказ в сутки);
прочие 4xx считаются окончательным отказом и не ретраятся.
Проверка подписи вручную (тело — байт-в-байт как получено):
# body.bin — тело вебхука, sig.b64 — значение X-Webhook-Signature
base64 -d sig.b64 > sig.bin
openssl dgst -sha256 -verify public.pem -signature sig.bin body.bin \
&& echo "подпись верна"11 Возвраты #
Два сценария в зависимости от настройки вашего клиента (refund_auto):
flowchart TD
A["POST /orders/{id}/refund
{ amount: RUB копейки } или {} — полный"] --> B{"refund_auto
у клиента?"}
B -- "true" --> C["сразу отправляется в СБП"]
C --> D["200 OK + вебхук
refunded / partially_refunded"]
B -- "false" --> E["заявка в пул на модерацию"]
E --> F["202 { status: pending_review }"]
F --> G{"решение оператора"}
G -- "одобрено" --> H["уходит в СБП → вебхук refunded / partially_refunded"]
G -- "отклонено" --> I["заявка rejected, статус заказа не меняется"]
style D fill:var(--ok-bg),stroke:var(--ok-text)
style H fill:var(--ok-bg),stroke:var(--ok-text)
style F fill:var(--warn-bg),stroke:var(--warn-text)
style I fill:var(--th-bg),stroke:var(--muted)
- Сумма возврата — рубли (копейки); пустое тело = полный возврат списанного.
- Частичных возвратов может быть несколько, суммарно не больше
rub_amountзаказа. - Поле
reason(необязательно, до 500 символов) — причина, видна оператору в пуле заявок. - Поле
currencyв запросе игнорируется — возврат всегда в рублях.
12 Деньги и курс #
- Все суммы — целые, в минорных единицах: USD центы (
10000= $100.00), RUB копейки (8476000= 84 760,00 ₽). rub_amount=round(amount/100 × курс × (1 + markup%) × 100)— считает сервис.- Курс фиксируется в заказе на момент создания (
rate_used) и больше не меняется. - Курс USD/RUB собирается каждые несколько секунд; если свежего курса нет —
создание заказа отвечает
503. - Возвраты — в рублях (копейки); без поля
amount— полный возврат списанного.
13 Формат ошибок #
{ "error": "amount must be positive" }| HTTP | Когда |
|---|---|
| 400 | Невалидный запрос, лимиты клиента, чужой idempotency_key |
| 401 | Нет/неверный токен, клиент на паузе |
| 403 | Заказ принадлежит другому клиенту |
| 404 | Заказ не найден |
| 503 | Нет свежего курса; создание заказов приостановлено |
| 502 | СБП недоступна |
14 Идемпотентность #
Передайте idempotency_key — повторный запрос с тем же ключом вернёт
тот же заказ без повторного списания. Ключ уникален глобально и привязан к вашему
клиенту: совпадение с чужим ключом даст 400 idempotency_key is already used,
а не чужой заказ.
for i in 1 2; do
curl -s -X POST "$API/api/v1/orders" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"amount":100,"return_url":"https://example.com/ok","idempotency_key":"dup-1"}' \
| jq -r .order_id
done
# оба раза напечатает один и тот же order_idДокументация: версия для LLM (llms.txt) · обновляется вместе с API.