Перейти к содержанию
STONS

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

Страница оплаты — собственная страница сервиса, на которой покупатель оплачивает инвойс: она показывает сумму, адрес и статус платежа, а когда у инвойса появляется исход — возвращает покупателя на сайт мерчанта подписанным редиректом. Прочитайте этот документ, если отправляете покупателей по checkout_url: в нём — что делает страница, адреса возврата, подпись редиректа и маршруты, которыми пользуется сервер страницы.

Главное правило

Не отдавайте товар по редиректу. Получив покупателя на return_url, проверьте подпись, затем запросите инвойс GET /v1/invoices/{id} и отдавайте товар только по ответу API — так же, как после вебхука.

Подпись доказывает одно: адрес собрал сервис для этого инвойса в этом статусе. Она не мешает покупателю открыть ту же ссылку ещё раз, пока её ts ещё принимается, и не заменяет проверку статуса.

Как это работает

  1. Продукт создаёт инвойс: POST /v1/invoices. Поле ответа checkout_url — ссылка на страницу оплаты этого инвойса. Если в установке нет страницы оплаты, поле равно null — см. инвойсы.
  2. Продукт отправляет покупателя по checkout_url.
  3. У инвойса, созданного без asset, страница сначала даёт покупателю выбрать сеть, затем монету — см. выбор токена. Затем показывает сумму, адрес и статус и обновляет их, пока покупатель платит.
  4. Когда у инвойса появляется исход, страница отправляет покупателя на адрес возврата или отмены мерчанта с параметрами и подписью. Пока инвойс не оплачен, кнопка «Вернуться в магазин» ведёт на адрес отмены с теми же параметрами и подписью.
  5. Продукт проверяет подпись и берёт статус из 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. Дальше:

  1. Сеть — выпадающий список сетей, в которых API предлагает хотя бы один актив: знак и название из каталога. Сеть не выбирается заранее.
  2. Монета — второй выпадающий список, доступный после выбора сети: только активы выбранной сети, каждый с каталожной иконкой, тикером и суммой к оплате из API по текущему курсу. Даже единственная монета требует явного выбора. Смена сети сбрасывает выбранную монету; совпадающий тикер не переносит выбор в другую сеть.
  3. Сводка — одна строка «К оплате»: сумма в выбранной монете и название сети; под ней мелко курс и, если комиссию сервиса платит покупатель, её ставка. Ниже одна строка: отправлять только эту монету в этой сети. Суммы разбивки «Цена · Комиссия сервиса · Итого» страница показывает после выбора, под «Подробнее».

Экран не добавляет нативные монеты или другие активы, которых нет в 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
}