Интеграция под ключ с ИИ-ассистентом
Этот документ — задание для ИИ-ассистента в проекте мерчанта: 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. Что спросить у человека
Прежде чем писать код, получите у владельца проекта:
- Ключ API. Владелец выпускает его в настройках проекта в кабинете — см. аутентификацию.
- Секрет вебхуков. Кабинет показывает его один раз, вместе с первым ключом, при создании проекта — см. вебхуки.
- Прошёл ли проект проверку. Пока проект не одобрен, инвойсы не создаются:
403 merchant_not_approved. Ключи при этом уже работают, так что код писать можно — см. коды ошибок. - Режим адресов проекта:
per_invoice— новый адрес под каждый инвойс,per_user— постоянный адрес пользователя. Проект создаётся вper_invoice;per_user— ограниченный режим, его разрешает оператор по проектам, и только после этого проект можно перевести в него. Режим может измениться позже, и смена решает, где будет собран следующий платёж: уже выданные адреса остаются при своём режиме и зачисляются как раньше — см. адреса. Два отказа значат разное:422 address_mode_disabled— платформа приостановила режим для всех (режим на паузе);422 mode_not_allowed— этому проекту режим не разрешён. Спрашивайте у владельца, в каком режиме проект сейчас, а не считайте режим неизменным. - Как показывать оплату: своей страницей в продукте или страницей оплаты. Чтобы задать верный вопрос, используйте правила шага 3.
- Токены: какие активы продукт предлагает. Приём работает в TRON, в семи EVM-сетях, в TON и в Solana — в USDT, USDT0, USDC и нативной монете каждой сети;
USDC_TRONоператор выключил целиком — см. сети и токены. Не зашивайте этот список в код. Активы, которые проект может принять прямо сейчас, с сетью, числом знаков и минимальной суммой каждого отдаётGET /v1/assets; какие токены принимает проект, решают его настройки в кабинете — см. список активов. - Кто платит комиссию сервиса — настройка проекта «Кто платит комиссии». Если платит покупатель, сумма к оплате больше цены — см. комиссии.
- Для страницы оплаты: адреса возврата и отмены, которые владелец задаёт в настройках проекта как «После успешной оплаты» и «Вернуться в магазин / при неудаче» — см. адреса возврата.
- Нужны ли выплаты. Если нужны, каждый адрес для выплат оператор должен заранее внести в вайтлист — см. выплаты.
Когда обработчик вебхуков готов, направьте проект на него сами: 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 на адрес вебхуков проекта. Обработчик — см. вебхуки:
- Читает сырое тело запроса, до разбора JSON.
- Проверяет подпись:
X-Signature: sha256=<hex>— это HMAC-SHA256 секретом вебхуков по строке<X-Timestamp>.<сырое тело>. Отклоняет вебхук, еслиX-Timestampотличается от текущего времени больше чем на 5 минут, и сравнивает подписи за постоянное время. Код проверки на Node.js или Python и пример для проверки реализации возьмите из подписи. - Отбрасывает дубли по
idсобытия: одно событие может прийти несколько раз. - Отвечает
2xxза 10 секунд, в том числе на типы событий, которые не обрабатывает, а работу выполняет асинхронно: без ответа2xxвебхук доставляется повторно. - Не доверяет телу. Для
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. Обработчик — см. подпись редиректа:
- Проверяет подпись: HMAC-SHA256 секретом вебхуков по строкам
redirect,ts,invoice_id,status,order_id, соединённым символом\n, и окноtsв 5 минут. Код проверки и пример — в разделе по ссылке. - Проверяет, что
invoice_id— инвойс этогоorder_id. - Берёт статус из
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}. - Весь путь проверен на небольшой сумме, и чек-лист перед продом пройден.