Страница оплаты
Страница оплаты — собственная страница сервиса, на которой покупатель оплачивает инвойс: она показывает сумму, адрес и статус платежа, а когда у инвойса появляется исход — возвращает покупателя на сайт мерчанта подписанным редиректом. Прочитайте этот документ, если отправляете покупателей по checkout_url: в нём — что делает страница, адреса возврата, подпись редиректа и маршруты, которыми пользуется сервер страницы.
Главное правило
Не отдавайте товар по редиректу. Получив покупателя на
return_url, проверьте подпись, затем запросите инвойсGET /v1/invoices/{id}и отдавайте товар только по ответу API — так же, как после вебхука.
Подпись доказывает одно: адрес собрал сервис для этого инвойса в этом статусе. Она не мешает покупателю открыть ту же ссылку ещё раз, пока её ts ещё принимается, и не заменяет проверку статуса.
Как это работает
- Продукт создаёт инвойс:
POST /v1/invoices. Поле ответаcheckout_url— ссылка на страницу оплаты этого инвойса. Если в установке нет страницы оплаты, поле равноnull— см. инвойсы. - Продукт отправляет покупателя по
checkout_url. - У инвойса, созданного без
asset, страница сначала даёт покупателю выбрать сеть, затем монету — см. выбор токена. Затем показывает сумму, адрес и статус и обновляет их, пока покупатель платит. - Когда у инвойса появляется исход, страница отправляет покупателя на адрес возврата или отмены мерчанта с параметрами и подписью. Пока инвойс не оплачен, кнопка «Вернуться в магазин» ведёт на адрес отмены с теми же параметрами и подписью.
- Продукт проверяет подпись и берёт статус из API.
Вебхуки при этом приходят так же, как без страницы.
Страница для покупателя
Страница открывается по checkout_url без входа и без ключей. Другой сайт не может встроить её во фрейм, и страница не передаёт сайту мерчанта Referer.
В статусе created страница показывает:
- название мерчанта;
- статус «Ожидаем оплату»;
- время до срока оплаты;
- сумму к оплате —
amount_expected— с тикером и сетью, с кнопкой копирования; - сеть: её знак по
network_icon_slug, названиеnetwork_nameи стандарт её токеновtoken_standard, например «BNB Smart Chain · Токен BEP-20». У нативной монеты сети — BNB, ETH, TRX, GRAM, SOL — стандарта токенов нет: она не токен, и строки «Токен …» страница не показывает; - под «Подробнее», свёрнуто: курс котировки; если комиссию сервиса платит покупатель — разбивку «Цена · Комиссия сервиса · Итого»:
amount_requested, комиссию со ставкой иamount_expected; показанная комиссия —amount_expectedминусamount_requested; - там же — уже полученную сумму, если
amount_receivedбольше нуля; - предупреждение отправлять ровно этот актив и в этой сети, одной строкой вместе с тем, что комиссия сети — сверх суммы;
- QR-код;
- адрес целиком, с кнопкой копирования;
- в TON — кнопку «Открыть в кошельке»: ссылку
wallet_url, которая открывает приложение кошелька с уже заполненными адресом, суммой и токеном; - кнопку «Вернуться в магазин», если у инвойса или проекта есть адрес отмены.
Сумма, сеть и адрес стоят в одной карточке друг под другом. Адрес занимает свою строку во всю ширину карточки, кнопка копирования рядом с ним — значок; в колонке шириной 512 px адрес до 48 знаков помещается в одну строку, на узком экране переносится. QR-код стоит под адресом. На экране меньше 640 px он скрыт за кнопкой «Показать QR-код»: телефон не сканирует собственный экран; на экране шире код показан сразу. Время до срока — справа в строке с названием мерчанта.
Суммы страница пишет без нулей в конце: 49.900000 показывается и копируется как 49.9.
Когда на инвойс пришли переводы, страница перечисляет их под заголовком «Полученные переводы»: сумма, подтверждения из нужных, хэш транзакции целиком с кнопкой копирования и ссылка на транзакцию в обозревателе сети. Ссылку страница показывает, только если это адрес https; без неё остаётся хэш.
QR-код содержит только адрес. Кошелёк, считавший его, не получает ни суммы, ни токена. Поэтому сумма показывается и копируется отдельно, а страница просит покупателя сверить адрес целиком после вставки. Адрес нигде не сокращается. В TON кнопка «Открыть в кошельке» передаёт ещё и сумму с токеном — ссылкой ton://transfer из документации TON; покупатель всё равно сверяет их в кошельке перед подтверждением.
Страница бывает на английском и на русском. Чтобы открыть её на нужном языке, добавьте к checkout_url параметр lang: ?lang=en или ?lang=ru; страница запоминает этот выбор в cookie. Без параметра страница берёт по порядку запомненный выбор, язык браузера и английский. Переключить язык покупатель может внизу страницы.
Страница сама обновляет статус, перезагружать её не нужно:
- первый запрос — через 3 секунды, второй — через 5, дальше каждые 10;
- после смены статуса отсчёт начинается заново с 3 секунд;
- пока вкладка скрыта, запрос идёт раз в 30 секунд;
- когда покупатель возвращается на вкладку, следующий запрос идёт через 3 секунды;
- неудачный запрос не меняет экран, и страница продолжает спрашивать.
Время до срока считается по часам сервера страницы, а не устройства покупателя. Без JavaScript страница не обновляется, не ведёт отсчёт, не копирует и не переводит на сайт.
Что страница показывает по статусам:
| Статус | Что видит покупатель |
|---|---|
created |
Перечисленное выше |
seen |
«Платёж замечен», число подтверждений сети из нужных, оценку оставшегося ожидания по confirmation_estimate — «Осталось около N минут», «Осталось меньше минуты» или «Почти готово» — и время до срока. Своего времени для сети страница не пишет: сколько ждать, зависит от числа подтверждений актива и времени блока сети, а их меняет администратор. В сети, которая ждёт ещё и окончательного блока, страница добавляет, что может выйти немного дольше. Страница не пишет, что больше ничего отправлять не нужно: если покупатель отправил не всю сумму, остаток можно дослать на тот же адрес до конца срока, а после подтверждения страница покажет, сколько осталось. Адрес, QR-код и кнопка «Вернуться в магазин» остаются; сумма счёта — для справки, без кнопки копирования и без разбивки, а кнопки «Открыть в кошельке» нет: пришедший перевод может уже покрывать сумму |
partially_paid |
«Пришла часть суммы» и время до срока. Вместо суммы к оплате — «Осталось доплатить»: amount_remaining с кнопкой копирования; сумма счёта и уже полученная сумма — под «Подробнее». Адрес, QR-код и кнопка «Вернуться в магазин» остаются; в TON «Открыть в кошельке» подставляет amount_remaining. Короткая подсказка под предупреждением: при выводе с биржи выводить с запасом на её комиссию за вывод, чтобы на адрес пришёл весь остаток |
confirmed, settled |
«Оплата получена» |
paid_late |
«Платёж пришёл после срока»: что делать с заказом, решает магазин |
underpaid |
«Сумма пришла не полностью». Если адреса отмены нет — ещё просьбу не отправлять остаток на этот адрес и связаться с магазином |
expired |
«Срок оплаты истёк» и просьбу не отправлять деньги повторно, если перевод уже ушёл, а связаться с магазином |
В статусе с исходом — confirmed, settled, paid_late, underpaid или expired — страница скрывает сумму, адрес и QR-код и перестаёт обновляться. Список переводов остаётся, а вместо адреса появляется ссылка на адрес оплаты в обозревателе сети. Дальше:
- Есть адрес нужного вида (см. адреса возврата) — страница отсчитывает 5 секунд и переводит покупателя на сайт мерчанта. Кнопка переводит сразу: «Перейти в магазин» — на адрес возврата, «Вернуться в магазин» — на адрес отмены. Прямо перед переходом страница запрашивает заново подписанный адрес, а не берёт полученный раньше; если запрос не удался, страница останавливается и просит нажать кнопку ещё раз. Так же страница ведёт себя, если покупатель открыл её у инвойса, у которого исход уже есть.
- Адреса нужного вида нет — страница показывает исход и оставляет покупателя на месте.
Пока у инвойса нет исхода, страница никуда не переводит покупателя сама. Кнопка «Вернуться в магазин» при нажатии запрашивает заново подписанный адрес отмены. Если за это время у инвойса появился исход, страница показывает его вместо перехода, а затем переводит, как при любом исходе.
expired и underpaid ещё могут смениться на paid_late, если деньги придут позже. Покупатель, открывший страницу снова, увидит новый статус; мерчант получит вебхук invoice.paid_late.
Несуществующий инвойс и инвойс отключённого мерчанта показывают одинаковую страницу «Счёт не найден». Страница ограничивает число запросов с одного IP-адреса в минуту и сверх лимита показывает «Слишком много запросов».
Выбор токена
Инвойс, созданный без asset (инвойсы), пока его статус created, открывается экраном выбора. Сверху — название мерчанта и время до срока, под ними одной строкой сумма заказа в USD. Дальше:
- Сеть — выпадающий список сетей, в которых API предлагает хотя бы один актив: знак и название из каталога. Сеть не выбирается заранее.
- Монета — второй выпадающий список, доступный после выбора сети: только активы выбранной сети, каждый с каталожной иконкой, тикером и суммой к оплате из API по текущему курсу. Даже единственная монета требует явного выбора. Смена сети сбрасывает выбранную монету; совпадающий тикер не переносит выбор в другую сеть.
- Сводка — одна строка «К оплате»: сумма в выбранной монете и название сети; под ней мелко курс и, если комиссию сервиса платит покупатель, её ставка. Ниже одна строка: отправлять только эту монету в этой сети. Суммы разбивки «Цена · Комиссия сервиса · Итого» страница показывает после выбора, под «Подробнее».
Экран не добавляет нативные монеты или другие активы, которых нет в choices: поддержка способа оплаты определяется ядром, а не наличием иконки.
Кнопка «Перейти к оплате» сохраняет выбор, и страница показывает экран оплаты, описанный выше. Кнопка видна на экране любой высоты: если конец формы ниже края экрана, кнопка прилипает к нижнему краю. Под кнопкой — строка о том, что после этого шага монету и сеть сменить нельзя.
- Показанные и записанные суммы. Сумму каждой монеты страница показывает по курсу, который API вернул вместе с ней, и отправляет
rate_idэтого курса вместе с выбором. Сервис записывает ровно показанную сумму, пока этот курс ещё свежий; иначе переводит цену по последнему свежему курсу. После выбора страница показывает записанную сумму. В токене, привязанном к доллару США, записанная сумма действует до дедлайна инвойса; в любом другом токене остаток долга может пересчитаться после окна котировки — см. окно котировки. - Выбор окончательный. Повторное нажатие с тем же токеном ничего не меняет. Если уже выбран другой токен, например в другой вкладке, страница показывает экран оплаты в этом токене с пометкой, что сменить его нельзя.
- Другая вкладка. Экран выбора раз в 10 секунд проверяет инвойс и перезагружается, если токен выбран или статус уже не
created. - Срок. Когда время до срока доходит до нуля, экран сообщает, что срок вышел, и выключает кнопку. Когда сервис переводит инвойс в
expired, страница перезагружается, показывает «Срок оплаты истёк» и переводит на адрес отмены, если он есть. - Выбрать нечего. Если ни один актив нельзя предложить — магазин сейчас не принимает ни одного из них, мерчант не одобрен, платформа приостановила режим адресов магазина или срок вышел, — страница сообщает, что оплатить сейчас нельзя, и просит не отправлять деньги. Разрешённая монета, курс которой сейчас не свежий, не предлагается, пока курс снова не станет свежим; этот случай API отмечает полем
rates_unavailable. - Токен стал недоступен к моменту нажатия — страница просит выбрать другой. Если сохранить выбор не удалось по другой причине, страница просит попробовать ещё раз через минуту.
- Без JavaScript интерактивный селектор скрыт; работает обычная форма с группами по сетям и вариантами монет с суммами внутри каждой группы.
Адреса возврата
Какой адрес получает покупатель, зависит от статуса инвойса:
| Статус | Куда | Почему |
|---|---|---|
created, seen, partially_paid |
адрес отмены — только кнопкой «Вернуться в магазин» | Инвойс ещё можно оплатить: страница не уводит покупателя сама |
confirmed, settled, paid_late |
адрес возврата | Инвойс оплачен; при paid_late платёж пришёл после срока, и решает мерчант |
expired, underpaid |
адрес отмены | Срок вышел без полной суммы |
Адреса проекта задаются в настройках проекта в кабинете — «После успешной оплаты» это адрес возврата, «Вернуться в магазин / при неудаче» — адрес отмены — или оператором. Пустое поле означает, что адреса нет. Без адреса нужного вида редиректа нет, а без адреса отмены нет и кнопки «Вернуться в магазин».
Для отдельного инвойса адрес можно переопределить полями return_url и cancel_url в POST /v1/invoices или в форме счёта в кабинете. Переопределение допускается, если его origin — схема, хост и порт — совпадает с origin одного из этих адресов проекта:
- адреса того же вида из настроек проекта;
- сайта проекта из заявки на проект.
Например, при сайте проекта https://shop.example допустимо https://shop.example/orders/1001/paid, а https://pay.shop.example/… и https://shop.example:8443/… — нет. На общих хостах t.me и telegram.me под одним origin живут разные боты и каналы, поэтому там совпасть должен и первый сегмент пути — имя бота или канала, без учёта регистра. При сайте проекта https://t.me/example_shop_bot допустимо https://t.me/example_shop_bot?start=paid_1001, а https://t.me/other_bot — нет. Адрес проекта на общем хосте без имени в пути, например https://t.me, не допускает ничего.
Если совпадения нет или у проекта нет ни адреса того же вида, ни сайта, ответ — 422 redirect_url_not_allowed, и сообщение называет допустимые origin. Адреса проекта меняются в кабинете или оператором, но никогда ключом API: утёкший ключ не отправит покупателей на сайт или к боту, которых нет в проекте.
Требования к любому адресу возврата или отмены:
- начинается с
https://в нижнем регистре; - без символа
@и без пробелов — так в адрес не попадают логин и пароль; - не длиннее 2000 символов;
- без параметров
invoice_id,order_id,status,tsиsignature: их добавляет сервис.
Остальные параметры и фрагмент сохраняются как есть. Используется переопределение инвойса, если оно есть, иначе адрес проекта на момент редиректа: смена адреса проекта сразу касается инвойсов без переопределения.
Переход до исхода
На адрес отмены покупатель может прийти, пока инвойс открыт: со status=created, status=seen или status=partially_paid, если нажал «Вернуться в магазин». Такой инвойс ещё может быть оплачен; при seen перевод уже замечен в сети, а при partially_paid часть суммы уже пришла. Не отменяйте заказ по такому переходу: статус берите из GET /v1/invoices/{id} и вебхуков, как после любого редиректа.
Если вы переходите с Heleket
| Heleket | STONS | Как устроено здесь |
|---|---|---|
url_success |
return_url |
Задаётся в настройках проекта или в инвойсе. После оплаты страница переводит покупателя сама через 5 секунд, кнопкой — сразу |
url_return |
cancel_url |
Задаётся там же. Пока инвойс не оплачен — кнопка «Вернуться в магазин»; после срока без полной суммы страница переводит сама |
lifetime |
expires_in |
От 300 до 259200 секунд (72 часа), по умолчанию 3600 — см. инвойсы |
order_id |
order_id |
Уникален в пределах проекта: 1–200 видимых ASCII-символов. Повтор с теми же данными возвращает созданный инвойс, с другими — 409 order_id_conflict |
url_callback |
адрес вебхуков проекта | Один на проект, задаётся в настройках проекта, а не в инвойсе — см. вебхуки |
Адреса в инвойсе допускаются только в пределах сайтов проекта — см. адреса возврата. К каждому редиректу сервис добавляет параметры и подпись, описанные ниже.
Параметры редиректа
| Параметр | Значение |
|---|---|
invoice_id |
Идентификатор инвойса |
order_id |
Идентификатор заказа мерчанта |
status |
Статус инвойса в момент, когда сервис собрал адрес; после кнопки «Вернуться в магазин» — created, seen или partially_paid |
ts |
Момент, когда сервис собрал адрес, целые секунды Unix |
signature |
Подпись, 64 шестнадцатеричных символа в нижнем регистре |
Значения закодированы по правилам query-строки: читайте их через URLSearchParams или аналог, а не разбором строки вручную.
Подпись
Подпись — HMAC-SHA256 в hex с секретом вебхуков мерчанта в качестве ключа. Подписывается строка из пяти строк, соединённых символом перевода строки \n (байт 0x0A), без перевода строки в конце:
redirect
<ts>
<invoice_id>
<status>
<order_id>
Разделитель — перевод строки, а не точка, потому что order_id может содержать точки, а перевод строки не может содержать ни одно поле. Первая строка redirect не даёт подписи редиректа сойти за подпись вебхука тем же секретом.
Получатель обязан:
- отклонять редирект, если
tsотличается от текущего времени больше чем на 300 секунд (5 минут); - сравнивать подписи за постоянное время;
- проверять, что
invoice_id— это инвойс заказаorder_id, и брать статус из API, а не из параметра.
Проверка на Node.js:
import { createHmac, timingSafeEqual } from "node:crypto";
const TOLERANCE_SECONDS = 300;
// query — URLSearchParams запроса, пришедшего на return_url или cancel_url.
export function verifyRedirect({ query, secret, nowMs = Date.now() }) {
const fields = ["ts", "invoice_id", "status", "order_id"].map((name) => query.get(name));
const signature = query.get("signature");
if (fields.some((value) => value === null || value.includes("\n")) || signature === null) return false;
const [ts, invoiceId, status, orderId] = fields;
if (!/^\d{1,12}$/.test(ts) || !/^[0-9a-f]{64}$/.test(signature)) return false;
if (Math.abs(nowMs / 1000 - Number(ts)) > TOLERANCE_SECONDS) return false;
const expected = createHmac("sha256", secret)
.update(["redirect", ts, invoiceId, status, orderId].join("\n"))
.digest();
return timingSafeEqual(expected, Buffer.from(signature, "hex"));
}
Функция возвращает true только для неизменённого адреса, подписанного этим секретом не раньше чем 300 секунд назад. Секрет вебхуков показывается один раз, при создании проекта в кабинете, — см. вебхуки.
Пример для проверки своей реализации. При секрете example-webhook-secret и параметрах ts=1789300000, invoice_id=6f1d3c1e-2f7a-4f0e-9a51-0c8b3e2d7a44, status=confirmed, order_id=ord_1001 подпись равна 2af98109e8fad432cc8e511f5e73f542ed3be8913b8d2863fe360f5f31f8e651.
Маршруты страницы оплаты
Два маршрута ниже — не для мерчантов. Их вызывает сервер страницы оплаты по приватной сети: из интернета они недоступны и в описании OpenAPI их нет.
- Без ключа API. Страницу открывает покупатель, у которого ключа нет; инвойс открывается его идентификатором — случайным UUID, который доходит до покупателя внутри
checkout_url. - Отключённый мерчант. Его инвойсов для этих маршрутов не существует (
404 invoice_not_found): страница не должна продолжать собирать деньги мерчанту, которого выключил оператор или владелец. - Кэширование. Успешный ответ содержит
Cache-Control: no-store: в нём подписанные адреса. - Лимит запросов. Запросы с одного IP-адреса ограничены в окне 60 секунд тем же значением, что лимит по IP у
/v1, но отдельным счётчиком. Все запросы страницы приходят с адреса её сервера, поэтому этот лимит защищает API, но не разделяет покупателей: покупателей ограничивает сама страница.
Прочитать инвойс для страницы
GET /checkout/v1/invoices/{id} возвращает инвойс в том виде, в каком его показывает страница оплаты.
Запрос
| Параметр | Где | Тип | Обязательно | Описание |
|---|---|---|---|---|
id |
путь | string (UUID) | да | invoice_id |
Ответ
200. Все поля присутствуют всегда; поле, которое не применимо, равно null.
| Поле | Тип | Описание |
|---|---|---|
invoice_id |
string (UUID) | Идентификатор инвойса |
merchant_name |
string | Название мерчанта, которое видит покупатель |
status |
string | Статус инвойса — см. инвойсы |
asset |
string или null | Актив, например USDT_TRON; null, пока покупатель не выбрал токен |
asset_icon_slug |
string или null | Slug знака актива из assets.icon_slug, например usdt0; не выводится из тикера. null до выбора или если знак не задан |
symbol |
string или null | Тикер актива для показа: USDT; null, пока покупатель не выбрал токен |
network |
string или null | Идентификатор сети актива, например tron; null, пока покупатель не выбрал токен |
network_name |
string или null | Название сети для покупателя из реестра сетей: TRON; null, пока покупатель не выбрал токен |
network_icon_slug |
string или null | Slug знака сети из реестра сетей: tron. null, пока покупатель не выбрал токен или у сети нет знака; тогда страница рисует нейтральный знак с первой буквой названия |
token_standard |
string или null | Как кошельки называют токены сети: TRC-20; null, пока покупатель не выбрал токен, и у нативной монеты сети (у актива без контракта), которая не токен. Страница показывает сеть как «TRON (TRC-20)», а при null — одним названием |
address |
string или null | Адрес для платежа, целиком, в том виде, в каком его видит покупатель: в EVM-сетях — с контрольной суммой EIP-55, в TON — user-friendly без флага bounce, начинается с UQ (0Q в тестовой сети), см. адреса в TON, в Solana — base58 из 32–44 знаков, регистр которых нужно сохранять, см. адреса в Solana. null, пока покупатель не выбрал токен |
wallet_url |
string или null | Ссылка, которая открывает приложение кошелька с заполненным переводом. Только в TON: ton://transfer/<адрес>?amount=<сумма>, а у токена ещё &jetton=<мастер-контракт жетона>, как ссылку определяет документация TON (https://docs.ton.org/onboarding/wallet-apps/deep-links). amount — сколько осталось заплатить, в минимальных единицах актива: нанограммах GRAM, миллионных долях USDT; мастер-контракт — в user-friendly форме с флагом bounce. В Solana сервис ссылки на кошелёк не отдаёт: поле там остаётся null, покупатель копирует адрес и сумму со страницы, а кнопки «Открыть в кошельке» нет. null в других сетях, пока покупатель не выбрал токен, пока перевод ждёт подтверждений (seen), в статусе с исходом, после срока оплаты и пока котировка обновляется — всегда, когда точная сумма к отправке неизвестна |
address_explorer_url |
string или null | Страница адреса в обозревателе сети; null, пока покупатель не выбрал токен или у сети нет ссылки на обозреватель |
price |
object или null | Цена инвойса, созданного без asset: amount в долларах США с 6 знаками после точки, currency — USD. null у инвойса, созданного с asset |
choices |
array или null | У инвойса без токена — активы, которые можно выбрать сейчас, в порядке каталога, — см. таблицу ниже. Пустой массив — выбрать сейчас нечего: статус не created, срок вышел, мерчант не одобрен, платформа приостановила режим адресов мерчанта или ни один разрешённый актив не проходит проверки. null у инвойса с токеном |
rates_unavailable |
boolean | true, когда разрешённый актив не попал в choices только потому, что его курс сейчас не свежий; такой актив вернётся, как только курс станет свежим. У инвойса с токеном всегда false |
quote |
object или null | У инвойса, токен которого выбран: rate — курс сумм, долларов США за целую единицу токена с 18 знаками после точки; currency — USD; expires_at — до какого момента действуют суммы, см. окно котировки. null у инвойса, созданного с asset, до выбора и у инвойса, токен которого выбран до появления курсов |
amount_requested |
string или null | Цена мерчанта в единицах актива; null, пока покупатель не выбрал токен |
amount_expected |
string или null | Сумма к оплате со всеми знаками актива: цена и комиссия сервиса, если её платит покупатель; null, пока покупатель не выбрал токен |
amount_received |
string или null | Сумма подтверждённых платежей; null, пока покупатель не выбрал токен |
amount_remaining |
string или null | amount_expected минус amount_received, но не меньше нуля, как в инвойсе. Страница просит его доплатить в статусе partially_paid; null, пока покупатель не выбрал токен |
fees |
object | Комиссии инвойса, как в инвойсе. Разбивку страница показывает при fees.service.payer, равном customer, а fees.network не читает |
expires_at |
string | Срок оплаты |
confirmations_required |
integer или null | Сколько подтверждений нужно платежу в этом активе: 20 в боевой сети TRON, 1 в TON, 1 в Solana; null, пока покупатель не выбрал токен |
confirmation_estimate |
object или null | Пока перевод ждёт подтверждений (status у него seen): сколько ещё ждать самому медленному из таких переводов. remaining_blocks — сколько блоков не хватает до confirmations_required; remaining_seconds — эти блоки, умноженные на время блока сети из реестра сетей, с округлением вверх до целой секунды; waits_for_finality — true, если сеть засчитывает перевод, только когда его блок ещё и окончательный (тег finalized), и тогда ожидание может выйти дольше оценки. После нужной глубины оба числа — 0. Это оценка: блоки идут не строго по расписанию, а сканер читает сеть проходами. null, когда ни один перевод подтверждений не ждёт |
deposits |
array | Платежи по инвойсу: tx_hash, amount, confirmations, status (seen или confirmed) и explorer_url, как в инвойсе |
redirect |
object или null | В статусе с исходом, если у инвойса или проекта есть адрес этого вида: kind — return или cancel, url — подписанный адрес. Иначе null |
back_url |
string или null | В created, seen и partially_paid, если у инвойса или проекта есть адрес отмены: подписанный адрес отмены для кнопки «Вернуться в магазин». Иначе null |
Поля элемента choices:
| Поле | Тип | Описание |
|---|---|---|
asset |
string | Идентификатор актива: значение для сохранения выбора |
asset_icon_slug |
string или null | Slug знака актива из каталога, независимо от symbol; null, если знак не задан |
network |
string | Сеть актива |
network_name |
string | Название сети для покупателя из реестра сетей |
network_icon_slug |
string или null | Slug знака сети; null, если у сети знака нет |
token_standard |
string или null | Как кошельки называют токены сети; null у нативной монеты сети |
symbol |
string | Тикер актива |
decimals |
integer | Число знаков актива |
amount_requested |
string | Цена, переведённая в этот актив по курсу rate с округлением вверх |
amount_expected |
string | Сумма к оплате в этом активе по курсу rate, с комиссией сервиса, если её платит покупатель. Совпадает с amount_expected инвойса после выбора по этому курсу, если фиксированная часть комиссии за это время не изменилась |
min_amount |
string | Минимальный депозит актива |
rate |
string | Курс сумм: долларов США за целую единицу актива, с 18 знаками после точки. У актива, не привязанного к доллару США, наценка платформы уже учтена |
rate_id |
string | Идентификатор этого курса: его отправляют вместе с выбором, чтобы записались показанные суммы |
rate_expires_at |
string | До какого момента выбор с этим rate_id записывается по этому курсу. Выбор, отправленный позже, переводится по последнему свежему курсу, поэтому к этому моменту страница перечитывает инвойс |
Знаки актива и сети независимы. Страница не угадывает знак токена по symbol: неизвестный slug даёт нейтральный знак. Отсутствующий в ответе старого API или некорректный asset_icon_slug страница читает как null, не блокируя оплату.
Подписанные адреса в redirect и back_url собираются в момент запроса, и ts в них — этот момент. В ответ не попадают идентификатор мерчанта, его секреты, external_user_id, order_id отдельным полем и amount_credited: номер заказа доходит до покупателя только внутри подписанного адреса.
Ошибки
| HTTP | error.code |
Когда |
|---|---|---|
| 400 | validation_failed |
id — не UUID |
| 404 | invoice_not_found |
Инвойса нет, или его мерчант отключён |
| 429 | rate_limited |
Превышен лимит запросов с IP-адреса |
| 500 | internal_error |
Необработанная ошибка сервиса |
Пример
GET /checkout/v1/invoices/6f1d3c1e-2f7a-4f0e-9a51-0c8b3e2d7a44
200 — оплаченный инвойс, у проекта которого есть адрес возврата:
{
"invoice_id": "6f1d3c1e-2f7a-4f0e-9a51-0c8b3e2d7a44",
"merchant_name": "Shop",
"status": "confirmed",
"asset": "USDT_TRON",
"asset_icon_slug": "usdt",
"symbol": "USDT",
"network": "tron",
"network_name": "TRON",
"network_icon_slug": "tron",
"token_standard": "TRC-20",
"address": "TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH",
"address_explorer_url": "https://tronscan.org/#/address/TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH",
"wallet_url": null,
"price": null,
"choices": null,
"rates_unavailable": false,
"quote": null,
"amount_requested": "49.900000",
"amount_expected": "49.900000",
"amount_received": "49.900000",
"amount_remaining": "0.000000",
"fees": {
"service": { "payer": "merchant", "percent": "1.00", "fixed": "0.000000", "amount": "0.499000" },
"network": { "payer": "merchant", "quote": null, "quote_rate": null, "actual": null, "status": "included" }
},
"expires_at": "2026-09-11T21:03:05.531Z",
"confirmations_required": 20,
"confirmation_estimate": null,
"deposits": [
{
"tx_hash": "50d858e0985ecc7f60418aaf0cc5ab587f42c2570a884095a9e8ccacd0f6545c",
"amount": "49.900000",
"confirmations": 20,
"status": "confirmed",
"explorer_url": "https://tronscan.org/#/transaction/50d858e0985ecc7f60418aaf0cc5ab587f42c2570a884095a9e8ccacd0f6545c"
}
],
"redirect": {
"kind": "return",
"url": "https://shop.example/thanks?invoice_id=6f1d3c1e-2f7a-4f0e-9a51-0c8b3e2d7a44&order_id=ord_1001&status=confirmed&ts=1789300000&signature=2af98109e8fad432cc8e511f5e73f542ed3be8913b8d2863fe360f5f31f8e651"
},
"back_url": null
}
200 — инвойс без токена, покупатель которого платит комиссию сервиса 1%, а у проекта нет адреса отмены, при курсе USDT 0.9991 USD:
{
"invoice_id": "3b9e5c2a-7d41-4f6b-8a2e-5c1d9f0b7e63",
"merchant_name": "Shop",
"status": "created",
"asset": null,
"asset_icon_slug": null,
"symbol": null,
"network": null,
"network_name": null,
"network_icon_slug": null,
"token_standard": null,
"address": null,
"address_explorer_url": null,
"wallet_url": null,
"price": { "amount": "100.000000", "currency": "USD" },
"choices": [
{
"asset": "USDT_TRON",
"asset_icon_slug": "usdt",
"network": "tron",
"network_name": "TRON",
"network_icon_slug": "tron",
"token_standard": "TRC-20",
"symbol": "USDT",
"decimals": 6,
"amount_requested": "100.090082",
"amount_expected": "101.090983",
"min_amount": "1.000000",
"rate": "0.999100000000000000",
"rate_id": "48213",
"rate_expires_at": "2026-09-16T12:05:00.000Z"
}
],
"rates_unavailable": false,
"quote": null,
"amount_requested": null,
"amount_expected": null,
"amount_received": null,
"amount_remaining": null,
"fees": {
"service": { "payer": "customer", "percent": "1.00", "fixed": null, "amount": null },
"network": { "payer": "merchant", "quote": null, "quote_rate": null, "actual": null, "status": "included" }
},
"expires_at": "2026-09-11T21:03:05.531Z",
"confirmations_required": null,
"confirmation_estimate": null,
"deposits": [],
"redirect": null,
"back_url": null
}
API отдаёт плоский список активов, у каждого своя сеть; группировать их по монете — дело страницы.
Сохранить выбор токена
POST /checkout/v1/invoices/{id}/select записывает токен, который покупатель выбрал для инвойса, созданного без asset. Сервер страницы вызывает его, когда покупатель нажимает «Перейти к оплате».
Запрос
id в пути — invoice_id. Тело — JSON-объект с Content-Type: application/json; поле, которого нет в таблице, отклоняется с 400 validation_failed.
| Поле | Тип | Обязательно | Описание |
|---|---|---|---|
asset |
string | да | Один из choices[].asset, например USDT_TRON |
rate_id |
string | нет | rate_id того же элемента choices: курс, суммы по которому видел покупатель |
Ответ
200 с инвойсом в формате чтения инвойса для страницы — уже с токеном, адресом, суммами и quote и с choices, равным null.
- Выбор окончательный. Повторный запрос с тем же активом возвращает инвойс без изменений, даже после срока, поэтому запрос, ответ на который потерялся, безопасно повторить. Другой актив получает
409 asset_already_selected. То же у инвойса, созданного сasset. - Одновременные запросы обрабатываются по одному: первый записывает свой актив, остальные получают
200или409 asset_already_selectedпо правилу выше. - Когда выбор возможен: статус
created, и часы сервиса не дошли доexpires_at. - Проверки — по порядку, с теми же кодами, что при создании инвойса: мерчант одобрен; платформа не приостановила его режим адресов; актив есть в
currenciesинвойса, если у инвойса есть список; актив принимается к оплате; включён оператором и в настройках проекта; у него есть курс, и курс свежий; цена, переведённая по этому курсу, не меньше его минимального депозита. - Курс. С
rate_idцена переводится по этому курсу, если это принятый курс монеты этого актива и он ещё свежий. Иначе, как и безrate_id, — по последнему свежему курсу монеты.rate_idдругой монеты или старый не ошибка: он просто не используется. Выбор записывает первую котировку, её показываетquoteответа, — см. цену и курс. - Условия. Ставка комиссии и плательщики — записанные при создании инвойса; фиксированная часть комиссии — та, что у актива в момент выбора.
amount_expectedсчитается от переведённой цены так же, как у инвойса, созданного в этом активе. - Адрес. Мерчанту
per_invoiceвыводится новый адрес, мерчантуper_user— постоянный адрес пользователя изexternal_user_id.
Ошибки
| HTTP | error.code |
Когда |
|---|---|---|
| 400 | validation_failed |
id — не UUID; в теле нет asset, есть лишнее поле, asset — не идентификатор актива или rate_id — не идентификатор курса; сумма вместе с комиссией сервиса длиннее 78 цифр в минимальных единицах |
| 400, 413, 415 | bad_request |
Тело — не JSON или пустое (400), больше 16 КиБ (413) или с неподдерживаемым Content-Type (415) |
| 403 | merchant_not_approved |
Мерчант ещё не одобрен после проверки или отклонён |
| 404 | invoice_not_found |
Инвойса нет, или его мерчант отключён |
| 409 | asset_already_selected |
У инвойса уже другой актив |
| 409 | invoice_expired |
Срок вышел, или инвойс уже не в статусе created |
| 422 | address_mode_disabled |
Платформа временно приостановила режим адресов мерчанта — см. режим на паузе |
| 422 | asset_not_offered |
Актива нет в currencies инвойса, или у него нет курса |
| 422 | asset_not_supported |
Актив неизвестен или не принимается к оплате |
| 422 | asset_payments_disabled |
Оператор временно выключил приём в активе |
| 422 | asset_disabled_by_merchant |
Приём в активе выключен в настройках проекта |
| 422 | amount_below_minimum |
Цена в активе по использованному курсу меньше его минимального депозита |
| 429 | rate_limited |
Превышен лимит запросов с IP-адреса |
| 500 | internal_error |
Необработанная ошибка сервиса |
| 503 | rate_unavailable |
У монеты актива сейчас нет свежего курса. Ничего не записывается; покупатель может выбрать другой актив или попробовать позже |
| 503 | addresses_exhausted |
Закончились индексы деривации адресов. Практически недостижимо |
| 503 | service_unavailable |
Сервис не может вывести новый адрес в текущей конфигурации; сообщение называет причину |
Пример
POST /checkout/v1/invoices/3b9e5c2a-7d41-4f6b-8a2e-5c1d9f0b7e63/select
Content-Type: application/json
{"asset": "USDT_TRON", "rate_id": "48213"}
200 — инвойс без токена из предыдущего примера после выбора по показанному курсу:
{
"invoice_id": "3b9e5c2a-7d41-4f6b-8a2e-5c1d9f0b7e63",
"merchant_name": "Shop",
"status": "created",
"asset": "USDT_TRON",
"asset_icon_slug": "usdt",
"symbol": "USDT",
"network": "tron",
"network_name": "TRON",
"network_icon_slug": "tron",
"token_standard": "TRC-20",
"address": "TSeJkUh4Qv67VNFwY8LaAxERygNdy6NQZK",
"address_explorer_url": "https://tronscan.org/#/address/TSeJkUh4Qv67VNFwY8LaAxERygNdy6NQZK",
"wallet_url": null,
"price": { "amount": "100.000000", "currency": "USD" },
"choices": null,
"rates_unavailable": false,
"quote": { "rate": "0.999100000000000000", "currency": "USD", "expires_at": "2026-09-11T21:03:05.531Z" },
"amount_requested": "100.090082",
"amount_expected": "101.090983",
"amount_received": "0.000000",
"amount_remaining": "101.090983",
"fees": {
"service": { "payer": "customer", "percent": "1.00", "fixed": "0.000000", "amount": "0.000000" },
"network": { "payer": "merchant", "quote": null, "quote_rate": null, "actual": null, "status": "included" }
},
"expires_at": "2026-09-11T21:03:05.531Z",
"confirmations_required": 20,
"confirmation_estimate": null,
"deposits": [],
"redirect": null,
"back_url": null
}