Жизненный цикл, идемпотентность и то, как правильно обрабатывать незавершённые платежи.
| Поле | Описание |
|---|---|
id | Наш идентификатор, pay_… |
status | processing · succeeded · failed |
status_detail | Уточнение для незавершённых и отклонённых платежей |
external_id | Ваш идентификатор заказа, как вы его передали |
recipient | Получатель по данным СБП |
amount_rub | Сумма платежа в рублях |
amount_usdt | Сколько списано с вашего баланса |
rate | Курс, по которому посчитано списание |
created_at, settled_at | Время создания и завершения, UTC |
error | {code, message} для отклонённого платежа, иначе null |
metadata | Всё, что вы передали при создании |
processing ─┬─► succeeded деньги ушли получателю, списание окончательное
└─► failed платёж не прошёл, USDT вернулись на баланс
Терминальных статусов два. Пока платёж в processing, средства уже списаны
с баланса и ни возвращать, ни списывать их повторно мы не будем — исход станет известен
позже.
| Значение | Что произошло |
|---|---|
awaiting_gateway | Платёж передан шлюзу, ждём ответа |
manual_review | Шлюз не ответил окончательно. Мы дожимаем статус сами; деньги не возвращены, потому что платёж мог пройти |
gateway_declined | Отказ на стороне получателя или банка |
processing дольше двух часов, напишите в поддержку:
средства при этом ни потеряны, ни возвращены, вопрос решается вручную.
Заголовок Idempotency-Key обязателен для POST /v1/payments.
Правила простые:
Idempotency-Replayed: true, второго списания не будет;idempotency_conflict (409);Дополнительно мы не даём случайно оплатить один QR дважды: пока по тому же QR есть
незавершённый платёж, новый запрос получит duplicate_qr_in_flight (409).
Это страховка на случай ретрая без ключа идемпотентности.
Чтобы показать пользователю получателя и сумму до списания:
curl https://api.exppay.net/v1/qr/resolve \
-H "Authorization: Bearer $EXPRESSPAY_KEY" \
-H "Content-Type: application/json" \
-d '{"qr_url": "https://qr.nspk.ru/AS1A0000000000000000000000000001"}'
{
"qrc_id": "AS1A0000000000000000000000000001",
"recipient": "ООО «Ромашка»",
"amount_rub": "1450.00",
"currency": "RUB",
"purpose": "Оплата заказа",
"payable": true
}
https://qr.nspk.ru/… с фиксированной суммой.unsupported_qr),
статические QR без суммы (unsupported_variable_amount).GET /v1/balance → {"balance_usdt": "1250.482100", "currency": "USDT"}
GET /v1/rate → {"pair": "USDT/RUB", "rate": "83.6666", "as_of": 1789483018,
"max_age_sec": 300, "stale": false}
POST /v1/quote → {"amount_rub": "1450.00", "amount_usdt": "17.330692",
"rate": "83.6666", ...}
Курс фиксируется в момент создания платежа, и в объекте платежа возвращается именно тот,
по которому посчитано списание. Если биржевые данные устареют (поле stale),
мы на время приостановим платежи и ответим rate_unavailable — вместо того
чтобы считать по неактуальному курсу.
GET /v1/payments?limit=50&status=succeeded&starting_after=pay_9f3c1a2b4d5e
Постраничная выборка курсором: берите next_cursor из ответа и передавайте
его в starting_after, пока has_more равно true.