ExpressPay API

Платежи

Жизненный цикл, идемпотентность и то, как правильно обрабатывать незавершённые платежи.

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

ПолеОписание
idНаш идентификатор, pay_…
statusprocessing · 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, средства уже списаны с баланса и ни возвращать, ни списывать их повторно мы не будем — исход станет известен позже.

Что значит status_detail

ЗначениеЧто произошло
awaiting_gatewayПлатёж передан шлюзу, ждём ответа
manual_reviewШлюз не ответил окончательно. Мы дожимаем статус сами; деньги не возвращены, потому что платёж мог пройти
gateway_declinedОтказ на стороне получателя или банка
Никогда не считайте processing отказом Не создавайте повторный платёж по тому же QR и не возвращайте деньги клиенту, пока статус не стал терминальным. Обычно это занимает секунды, в редких случаях — минуты. Если платёж висит в processing дольше двух часов, напишите в поддержку: средства при этом ни потеряны, ни возвращены, вопрос решается вручную.

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

Заголовок Idempotency-Key обязателен для POST /v1/payments. Правила простые:

Дополнительно мы не даём случайно оплатить один QR дважды: пока по тому же QR есть незавершённый платёж, новый запрос получит duplicate_qr_in_flight (409). Это страховка на случай ретрая без ключа идемпотентности.

Проверка QR без оплаты

Чтобы показать пользователю получателя и сумму до списания:

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
}

Какие QR мы оплачиваем

Баланс, курс и предварительный расчёт

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.