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

Интеграция под ключ с ИИ-ассистентом

Этот документ — задание для ИИ-ассистента в проекте мерчанта: Claude Code, Cursor или любого другого, который пишет код по инструкции. В нём по порядку работ перечислено, что построить для приёма платежей через STONS, какие решения принять и как, и что проверить в конце. Каждое правило ссылается на документ API, в котором оно записано; при расхождении прав документ API.

Словарь

Кабинет — веб-приложение STONS, в котором владелец проекта управляет проектом. Кабинет и API называют некоторые вещи по-разному; эта документация пользуется терминами из первого столбца.

Термин В кабинете Что это
кабинет «Кабинет» Веб-приложение STONS по адресу https://app.stons.io, в котором владелец проекта управляет проектом
мерчант проект («Проекты») Сайт, бот или сервис, который принимает платежи со своими ключами API и настройками
владелец проекта учётная запись, создавшая проект Выпускает ключи API и задаёт адрес вебхуков, адреса возврата, токены, которые принимает проект, и кто платит комиссии. Видит доставки вебхуков и отправляет недоставленные заново
оператор поддержка Сотрудник STONS: проверяет проекты, ведёт вайтлист выплат, подтверждает выплаты, которые ждут человека, назначает комиссии и лимиты, разрешает проекту ограниченные режимы — постоянный адрес на пользователя и прямые сборы на собственный кошелёк проекта
инвойс счёт, в разделе «Платежи» Просьба заплатить конкретную сумму до дедлайна: POST /v1/invoices
ключ API «Ключ API» sk_<16 шестнадцатеричных символов>_<43 символа base64url>, передаётся в заголовке X-Api-Key. Показывается один раз, при выпуске
секрет вебхуков «Секрет подписи вебхуков» Ключ подписей HMAC-SHA256 у вебхуков и у редиректов со страницы оплаты. Показывается один раз, при создании проекта. Используется как строка, без преобразований
актив токен и сеть Идентификатор токена в сети, <ТИКЕР>_<СЕТЬ>: USDT_TRON, USDC_SOLANA
страница оплаты ссылка на оплату Собственная страница сервиса для одного инвойса, по ссылке checkout_url из инвойса
подтверждение — Блок, добавленный поверх блока с переводом. Перевод засчитывается, когда у него столько подтверждений, сколько требует его актив
пополнение — В режиме per_user: перевод на постоянный адрес пользователя, который не оплачивает ни один инвойс. Зачисляется на баланс проекта, а deposit.confirmed приходит с invoice_id, равным null
выплата — Перевод с горячего кошелька сервиса на адрес из вайтлиста мерчанта: POST /v1/payouts
вайтлист — Адреса для выплат мерчанта, по активам. Их вносит оператор; новый адрес становится доступен после задержки, по умолчанию через 24 часа

Шаг 0. Что спросить у человека

Прежде чем писать код, получите у владельца проекта:

  1. Ключ API. Владелец выпускает его в настройках проекта в кабинете — см. аутентификацию.
  2. Секрет вебхуков. Кабинет показывает его один раз, вместе с первым ключом, при создании проекта — см. вебхуки.
  3. Прошёл ли проект проверку. Пока проект не одобрен, инвойсы не создаются: 403 merchant_not_approved. Ключи при этом уже работают, так что код писать можно — см. коды ошибок.
  4. Режим адресов проекта: per_invoice — новый адрес под каждый инвойс, per_user — постоянный адрес пользователя. Проект создаётся в per_invoice; per_user — ограниченный режим, его разрешает оператор по проектам, и только после этого проект можно перевести в него. Режим может измениться позже, и смена решает, где будет собран следующий платёж: уже выданные адреса остаются при своём режиме и зачисляются как раньше — см. адреса. Два отказа значат разное: 422 address_mode_disabled — платформа приостановила режим для всех (режим на паузе); 422 mode_not_allowed — этому проекту режим не разрешён. Спрашивайте у владельца, в каком режиме проект сейчас, а не считайте режим неизменным.
  5. Как показывать оплату: своей страницей в продукте или страницей оплаты. Чтобы задать верный вопрос, используйте правила шага 3.
  6. Токены: какие активы продукт предлагает. Приём работает в TRON, в семи EVM-сетях, в TON и в Solana — в USDT, USDT0, USDC и нативной монете каждой сети; USDC_TRON оператор выключил целиком — см. сети и токены. Не зашивайте этот список в код. Активы, которые проект может принять прямо сейчас, с сетью, числом знаков и минимальной суммой каждого отдаёт GET /v1/assets; какие токены принимает проект, решают его настройки в кабинете — см. список активов.
  7. Кто платит комиссию сервиса — настройка проекта «Кто платит комиссии». Если платит покупатель, сумма к оплате больше цены — см. комиссии.
  8. Для страницы оплаты: адреса возврата и отмены, которые владелец задаёт в настройках проекта как «После успешной оплаты» и «Вернуться в магазин / при неудаче» — см. адреса возврата.
  9. Нужны ли выплаты. Если нужны, каждый адрес для выплат оператор должен заранее внести в вайтлист — см. выплаты.

Когда обработчик вебхуков готов, направьте проект на него сами: PATCH /v1/project с полем webhook_url, тем же ключом — в кабинет заходить никому не нужно (настройки проекта). GET /v1/project сначала покажет, что задано сейчас и одобрен ли проект. Пока адрес не задан, проект не получает событий, и события того времени позже не досылаются — см. доставку.

Тем же ключом задаются адреса возврата страницы оплаты (return_url, cancel_url) и выбираются принимаемые токены (GET и PUT /v1/project/assets). Два ограничения, которые стоит знать заранее: адрес вебхука можно заменить, но не удалить, а секрет подписи не возвращает ни один маршрут — попросите его у владельца один раз, при создании проекта.

Не придумывайте значения за человека: ключ и секрет существуют только у него.

Шаг 1. Переменные окружения

Ключ и секрет — только на сервере продукта, в менеджере секретов или переменных окружения. Их нет в браузере, мобильном приложении, репозитории и логах — см. чек-лист. Имена переменных выбирает проект, например:

PAYMENTS_API_URL=https://api.stons.io
PAYMENTS_API_KEY=sk_...
PAYMENTS_WEBHOOK_SECRET=...

Адрес API — с https://: по http:// ключ ушёл бы открытым текстом ещё до перенаправления — см. аутентификацию.

Шаг 2. Клиент API

  • Базовый адрес — https://api.stons.io/v1, ключ — в заголовке X-Api-Key, тела запросов и ответов — JSON с Content-Type: application/json.
  • Описание API в формате OpenAPI 3.1 — https://api.stons.io/v1/openapi.json. Тела запросов принимают только описанные поля: неизвестное поле получает 400 validation_failed.
  • Суммы — строки в единицах актива: "49.9" в запросе, "49.900000" в ответе. У суммы актива в ответе после точки ровно столько цифр, сколько знаков у актива, поэтому без точки она даёт целое число в минимальных единицах. В сумме запроса знаков после точки не больше, чем у актива — см. суммы.
  • Ошибка — тело {"error": {"code", "message", "details"}}. Решения принимайте по error.code, а не по тексту message — см. коды ошибок.
  • 429 rate_limited — повторить через число секунд из заголовка Retry-After, см. лимиты запросов.
  • 500 internal_error и 503 service_unavailable — повторить позже с тем же order_id или reference. Что делать при остальных кодах — в перечне кодов.
  • X-Request-Id из ответа сохраняйте в логах: по нему поддержка находит запрос.

Шаг 3. Выбрать, как показывать оплату

Примените первое подходящее правило:

  • Если монету и сеть должен выбрать покупатель, создайте инвойс без asset и отправьте покупателя по checkout_url (шаг 5Б). У такого инвойса нет адреса до выбора, поэтому показать его на своей странице нельзя. Если у сервиса нет страницы оплаты, запрос получит 422 asset_required: тогда примените следующее правило — см. токен выбирает покупатель.
  • Если цены у продукта в долларах, а монета известна заранее, создайте инвойс с asset и price_currency: "USD", передав цену в amount. Сервис сам пересчитает её по курсу и вернёт address, amount_expected и quote.rate в том же ответе — считать курс самим не нужно и не следует. Чтобы показать «≈ 0,45 SOL» до создания, спросите GET /v1/rates — см. цену в долларах в выбранной монете.
  • Если у продукта свой экран оплаты, создайте инвойс с asset и покажите на нём address и amount_expected (шаг 5А).
  • Иначе создайте инвойс с asset и отправьте покупателя по checkout_url (шаг 5Б). Если checkout_url равен null, страницы оплаты у сервиса нет: используйте шаг 5А.

Независимо от выбора:

  • Если режим per_user, передавайте external_user_id в каждом инвойсе. Постоянный адрес пользователя без инвойса выдаёт POST /v1/addresses — см. адреса.
  • Если продукт зачисляет пополнения per_user на балансы пользователей, проверяйте каждый deposit.confirmed с invoice_id, равным null, запросом GET /v1/deposits/{tx_hash} и зачисляйте пользователю, только когда у перевода с log_index из события status = confirmed и credited = true, — см. проверку перевода.

Шаг 4. Создать инвойс

POST /v1/invoices — см. создание инвойса.

curl -s -X POST https://api.stons.io/v1/invoices \
  -H "X-Api-Key: $PAYMENTS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"order_id": "ord_1001", "asset": "USDT_TRON", "amount": "49.9", "expires_in": 3600}'
  • order_id — номер заказа в продукте, 1–200 видимых ASCII-символов без пробелов. Это ключ идемпотентности: при таймауте повторяйте запрос с тем же order_id и теми же полями — вернётся тот же инвойс с кодом 200 вместо 201, второй не создастся. Тот же order_id с другой суммой, активом, пользователем или адресами возврата получает 409 order_id_conflict — см. идемпотентность.
  • expires_in — необязательно: от 300 (5 минут) до 259200 секунд (72 часа), по умолчанию 3600.
  • Сумма не меньше минимального депозита актива, иначе 422 amount_below_minimum.
  • Сохраните у заказа invoice_id: по нему дальше проверяется статус.

Без asset поле amount — цена в долларах США, а необязательный currencies — из каких активов выбирает покупатель. В ответе asset, network, address и суммы в активе — null, а цена лежит в price:

curl -s -X POST https://api.stons.io/v1/invoices \
  -H "X-Api-Key: $PAYMENTS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"order_id": "ord_1002", "amount": "49.9"}'

Шаг 5А. Оплата на своей странице

Покажите покупателю из ответа:

  • address — целиком, без сокращений вида «начало…конец»: на сокращённые адреса охотятся подменой похожим адресом — см. показ адреса;
  • amount_expected и актив. Это сумма к оплате: если комиссию сервиса платит покупатель, она больше цены заказа, а сама цена — в amount_requested, см. комиссии;
  • сеть актива по названию из network_name — TRON, одна из EVM-сетей, TON или Solana — с предупреждением, что перевод другого токена или в другой сети не зачисляется автоматически — см. чек-лист;
  • срок оплаты, expires_at;
  • payment_uri — ссылка, открывающая кошелёк с заполненным переводом, если она не null: покупателю не придётся ни копировать адрес, ни набирать сумму. Сейчас её даёт TON; в Solana и остальных сетях она null, и адрес с суммой копируют со страницы;
  • QR-код — попросите его параметром include=qr при создании или чтении инвойса и покажите qr.address как src у <img>. В нём тот же адрес, и это та же картинка, которую рисует наша страница оплаты, — библиотека QR на вашей стороне не нужна, см. QR-код.

Что не меняется от инвойса к инвойсу — число подтверждений актива (confirmations), его метку (icon), стандарт токена и минимальную сумму — берите один раз из GET /v1/assets и держите у себя: по confirmations рисуется «2 из 20», пока перевод набирает подтверждения.

Проекту могут быть включены прямые сборы: деньги каждого платежа пересылаются на собственный кошелёк проекта за вычетом комиссии сервиса, а не хранятся у нас. Режим разрешает проекту оператор; кошелёк владелец проекта вписывает сам в кабинете, а сотрудник сервиса его одобряет — до одобрения адрес денег не получает. В API от этого не меняется ничего, и товар в любом случае отдаётся на confirmed; меняется смысл двух вещей: баланс такого проекта остаётся нулевым, потому что деньги уже на его кошельке, а settled значит этот кошелёк, а не хранилище сервиса. В активе, по которому действующего кошелька у проекта нет, платёж уходит в хранилище, и проект получает его заявкой на вывод, как любой другой, — так что нулевой баланс тоже не обещание. Не делайте интеграцию, которая ждёт роста баланса, — см. куда уходят деньги.

Пришедшие деньги читаются запросом GET /v1/balance: баланс по реестру по каждому активу, что ещё не подтверждено, и то и другое в долларах — см. баланс.

Статус узнавайте из вебхуков (шаг 6) и запросом GET /v1/invoices/{id}. Если вебхук не дошёл, пройдите ленту изменений — шаг 7. Экран, который опрашивает инвойс, держит ETag последнего ответа и присылает его обратно в If-None-Match: «ничего не изменилось» тогда стоит одного 304 без тела, см. условные чтения.

Шаг 5Б. Страница оплаты

Отправьте покупателя по checkout_url из ответа. Если инвойс создан без asset, страница сначала даёт покупателю выбрать монету и сеть. Страница показывает сумму, адрес, QR-код и статус, а когда у инвойса появляется исход, возвращает покупателя на адрес возврата или отмены — см. страницу оплаты.

  • Для отдельного инвойса адреса можно переопределить полями return_url и cancel_url в POST /v1/invoices, но только в пределах сайтов проекта, иначе 422 redirect_url_not_allowed — см. адреса возврата.
  • Пока инвойс не оплачен, страница показывает кнопку «Вернуться в магазин» на адрес отмены со status=created, status=seen или status=partially_paid. Инвойс ещё можно оплатить: заказ по такому переходу не отменяйте — см. переход до исхода.

Шаг 6. Обработчик вебхуков

Сервис шлёт POST на адрес вебхуков проекта. Обработчик — см. вебхуки:

  1. Читает сырое тело запроса, до разбора JSON.
  2. Проверяет подпись: X-Signature: sha256=<hex> — это HMAC-SHA256 секретом вебхуков по строке <X-Timestamp>.<сырое тело>. Отклоняет вебхук, если X-Timestamp отличается от текущего времени больше чем на 5 минут, и сравнивает подписи за постоянное время. Код проверки на Node.js или Python и пример для проверки реализации возьмите из подписи.
  3. Отбрасывает дубли по id события: одно событие может прийти несколько раз.
  4. Отвечает 2xx за 10 секунд, в том числе на типы событий, которые не обрабатывает, а работу выполняет асинхронно: без ответа 2xx вебхук доставляется повторно.
  5. Не доверяет телу. Для invoice.* и для deposit.* с invoice_id запрашивает GET /v1/invoices/{id}, для deposit.* без invoice_id — GET /v1/deposits/{tx_hash}, для payout.* — GET /v1/payouts/{id}, и действует по ответу API — см. главное правило.

Порядок доставки не гарантирован: invoice.confirmed может прийти раньше invoice.seen. Ориентируйтесь на состояние из API.

Шаг 7. Когда отдавать товар

По статусу из GET /v1/invoices/{id} — см. статусы:

Статус Что делать
created, seen Ждать. seen — перевод замечен, но ещё может откатиться
partially_paid Не отдавать товар. До срока пришла часть суммы: если продукт показывает оплату сам, покажите покупателю amount_remaining и тот же адрес — см. частичную оплату
confirmed, settled Отдать товар. Переплата — тоже confirmed, amount_received больше amount_expected
underpaid Срок вышел, пришла часть суммы: решает продукт
expired Срок вышел без платежа: закрыть заказ
paid_late Платёж пришёл после срока: решает продукт
  • Свою цену сверяйте с amount_requested у инвойса, созданного с asset. У инвойса, созданного без asset, сверяйте с price.amount: amount_requested до выбора — null, а после — цена, переведённая по курсу, см. цену и курс.
  • expired и underpaid ещё могут стать paid_late, если деньги придут позже — см. страницу оплаты.
  • Выдачу товара делайте идемпотентной по order_id: повтор вебхука, второе событие того же изменения или редирект не должны выдать товар второй раз.

Шаг 8. Возврат со страницы оплаты

Если используется страница оплаты, покупатель приходит на адрес возврата после confirmed, settled или paid_late, а на адрес отмены — после expired или underpaid либо по кнопке «Вернуться в магазин». Параметры — invoice_id, order_id, status, ts и signature. Обработчик — см. подпись редиректа:

  1. Проверяет подпись: HMAC-SHA256 секретом вебхуков по строкам redirect, ts, invoice_id, status, order_id, соединённым символом \n, и окно ts в 5 минут. Код проверки и пример — в разделе по ссылке.
  2. Проверяет, что invoice_id — инвойс этого order_id.
  3. Берёт статус из GET /v1/invoices/{id}, а не из параметра. Товар по редиректу не отдаётся.

Шаг 9. Выплаты — если нужны

POST /v1/payouts — см. выплаты:

  • Платить можно только на адреса из вайтлиста мерчанта и только после их задержки. Адрес для выплаты никогда не берётся из истории транзакций.
  • reference — ключ идемпотентности: повтор с тем же reference, активом, адресом и суммой возвращает ту же выплату с кодом 200 вместо 202. Другие значения получают 409 reference_conflict — см. идемпотентность.
  • pending_review — не ошибка: выплата принята и ждёт решения человека, а review_reasons говорит почему.
  • 503 payouts_halted — повторить позже с тем же reference.
  • Исход берите из GET /v1/payouts/{id}. Вебхуки payout.* — сигнал сделать этот запрос, и не у каждой смены статуса он есть: какие статусы окончательные и какие переходы присылают вебхук — в статусах выплат.

Шаг 10. Перед запуском

Пройдите чек-лист перед продом целиком и проверьте весь путь на небольшой сумме: инвойс, оплата, вебхук, проверка статуса, выдача товара.

Чего не делать никогда

  • Не зашивайте в код число знаков и не превращайте суммы в числа с плавающей точкой: храните строки или целые числа в минимальных единицах — см. суммы.
  • Не сокращайте адрес нигде, где он показывается или пишется в лог — см. показ адреса.
  • Не приводите адрес к нижнему регистру и не нормализуйте его иначе. В Solana адрес — base58, и регистр — его часть: адрес, приведённый к нижнему регистру, — это адрес, ключа к которому ни у кого нет. Храните и сравнивайте адрес ровно таким, каким его вернул API, — см. идентификаторы и знаки после запятой.
  • Не отдавайте товар по телу вебхука или по редиректу без GET /v1/invoices/{id} — см. главное правило вебхуков и страницы оплаты.
  • Не отдавайте товар при seen.
  • Не разбирайте и не сериализуйте тело вебхука заново перед проверкой подписи — см. подпись.
  • Не создавайте второй инвойс или вторую выплату после таймаута: повторяйте запрос с тем же order_id или reference.
  • Не берите адрес для выплаты из истории транзакций — см. выплаты.
  • Не кладите ключ API и секрет вебхуков в браузер, мобильное приложение, репозиторий и логи — см. аутентификацию.
  • Не принимайте решений по error.message — см. коды ошибок.

Самопроверка

Прежде чем сообщить, что интеграция готова, подтвердите в коде каждый пункт:

  • Ключ и секрет читаются из окружения сервера и не пишутся в логи.
  • Каждый вызов API идёт на https://, передаёт X-Api-Key и обрабатывает error.code, 429 с Retry-After, а также 500 и 503 service_unavailable повтором с тем же ключом идемпотентности.
  • Суммы везде — строки или целые числа в минимальных единицах, в том числе в базе продукта.
  • Инвойс создаётся с постоянным order_id, а invoice_id сохраняется у заказа.
  • Страница показывает адрес целиком, amount_expected, актив и сеть — или покупатель уходит по checkout_url.
  • Обработчик вебхуков проверяет подпись по сырому телу с окном в 5 минут, отбрасывает дубли по id, отвечает 2xx за 10 секунд и проходит пример из подписи.
  • Товар отдаётся, только когда GET /v1/invoices/{id} возвращает confirmed или settled, и один раз на order_id.
  • У продукта есть решение для underpaid, paid_late и переплаты.
  • При partially_paid товар не отдаётся.
  • Обработчики адресов возврата и отмены проверяют подпись и сами не отдают товар и не отменяют заказ.
  • Выплаты, если они есть, создаются с постоянным reference, а исход берут из GET /v1/payouts/{id}.
  • Весь путь проверен на небольшой сумме, и чек-лист перед продом пройден.