Интеграция под ключ с ИИ-ассистентом
Этот документ — задание для ИИ-ассистента в проекте мерчанта: Claude Code, Cursor или любого другого, который пишет код по инструкции. Он собирает из документации STONS то, что нужно, чтобы подключить приём платежей от начала до конца, в порядке, в котором это делается. Каждое правило здесь — из документов API, на которые стоят ссылки; при расхождении прав документ API.
Шаг 0. Что спросить у человека
Прежде чем писать код, выясните у владельца проекта:
- Ключ API вида
sk_<16 шестнадцатеричных символов>_<43 символа base64url>. Его выдаёт оператор STONS, показывается он один раз — см. аутентификацию. - Секрет вебхуков. Выдаётся вместе с ключом при создании мерчанта — см. вебхуки.
- URL для вебхуков, который оператор указал у мерчанта. Сменить его может только оператор.
- Режим адресов мерчанта:
per_invoice— новый адрес под каждый инвойс,per_user— постоянный адрес пользователя. Режим задаёт оператор — см. адреса. - Как показывать оплату: своей страницей в продукте (адрес и сумма из ответа API) или страницей оплаты сервиса по ссылке
checkout_url. Во втором случае — адреса возврата и отмены, которые оператор задал мерчанту: без них страница не вернёт покупателя на сайт — см. страницу оплаты. - Активы:
USDT_TRON,USDC_TRONили оба — см. инвойсы.
Не придумывайте значения за человека: ключ и секрет существуют только у него.
Шаг 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. - Суммы — строки в единицах актива:
"49.9"в запросе,"49.900000"в ответе. Не превращайте их в числа с плавающей точкой — см. инвойсы. - Ошибка — тело
{"error": {"code", "message", "details"}}. Решения принимайте поerror.code, а не по текстуmessage— см. коды ошибок. 429 rate_limited— повторить через время из заголовкаRetry-After.X-Request-Idиз ответа сохраняйте в логах: по нему поддержка находит запрос.- Описание API в формате OpenAPI 3.1 —
https://api.stons.io/v1/openapi.json.
Шаг 3. Создать инвойс
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, второй не создастся. Тот жеorder_idс другой суммой, активом или пользователем —409 order_id_conflict.expires_in— от 60 до 604800 секунд.- В режиме
per_userобязателенexternal_user_id. - Сумма не меньше минимального депозита актива, иначе
422 amount_below_minimum. - Сохраните у заказа
invoice_id: по нему дальше проверяется статус.
Шаг 4А. Оплата на своей странице
Покажите покупателю из ответа:
address— целиком, без сокращений вида «начало…конец»: на сокращённые адреса охотятся подменой похожим адресом — см. адреса;amount_expectedи актив. Это сумма к оплате: если в проекте комиссию сервиса платит покупатель, она больше цены заказа, а сама цена — вamount_requested. Свою цену сверяйте сamount_requested— см. комиссии;- сеть — TRON, с предупреждением, что перевод другого токена или в другой сети не зачисляется автоматически — см. чек-лист.
Статус узнавайте из вебхуков (шаг 5) и запросом GET /v1/invoices/{id}.
Шаг 4Б. Страница оплаты сервиса
Отправьте покупателя по checkout_url из ответа. Если поле null, оператор не настроил страницу — используйте шаг 4А или обратитесь к оператору. Страница сама показывает сумму, адрес, QR-код и статус, а когда у инвойса появляется исход, возвращает покупателя на адрес возврата или отмены — см. страницу оплаты.
Для отдельного инвойса адреса можно переопределить полями return_url и cancel_url в POST /v1/invoices, но только с тем же origin, что у адресов мерчанта, иначе 422 redirect_url_not_allowed.
Шаг 5. Обработчик вебхуков
Сервис шлёт POST на URL мерчанта. Обработчик — см. вебхуки:
- Читает сырое тело запроса, до разбора JSON.
- Проверяет подпись:
X-Signature: sha256=<hex>— это HMAC-SHA256 секретом вебхуков по строке<X-Timestamp>.<сырое тело>. Отклоняет вебхук, еслиX-Timestampотличается от текущего времени больше чем на 5 минут. Сравнивает подписи за постоянное время. - Отбрасывает дубли по
idсобытия: одно событие может прийти несколько раз. - Отвечает
2xxсразу после проверки, а работу выполняет асинхронно: вебхук без ответа2xxза 10 секунд доставляется повторно. - Не доверяет телу. По
data.invoice_idзапрашиваетGET /v1/invoices/{id}и действует по ответу API.
Проверка подписи на Node.js — из вебхуков:
import { createHmac, timingSafeEqual } from "node:crypto";
const TOLERANCE_SECONDS = 300;
// rawBody — Buffer с сырым телом запроса, до JSON.parse.
export function verifyWebhook({ rawBody, signatureHeader, timestampHeader, secret, nowMs = Date.now() }) {
if (typeof timestampHeader !== "string" || !/^\d{1,12}$/.test(timestampHeader)) return false;
if (Math.abs(nowMs / 1000 - Number(timestampHeader)) > TOLERANCE_SECONDS) return false;
const match = /^sha256=([0-9a-f]{64})$/.exec(signatureHeader ?? "");
if (match === null) return false;
const expected = createHmac("sha256", secret).update(`${timestampHeader}.`).update(rawBody).digest();
return timingSafeEqual(Buffer.from(match[1], "hex"), expected);
}
Порядок доставки не гарантирован: invoice.confirmed может прийти раньше invoice.seen. Ориентируйтесь на состояние из API.
Шаг 6. Когда отдавать товар
По статусу из GET /v1/invoices/{id} — см. статусы:
| Статус | Что делать |
|---|---|
created, seen |
Ждать. seen — перевод замечен, но ещё может откатиться |
confirmed, settled |
Отдать товар. Переплата — тоже confirmed, amount_received больше amount_expected |
underpaid |
Срок вышел, пришла часть суммы: решает продукт |
expired |
Срок вышел без платежа: закрыть заказ |
paid_late |
Платёж пришёл после срока: решает продукт |
Выдачу товара делайте идемпотентной: повтор вебхука или редиректа не должен выдать товар второй раз.
Шаг 7. Возврат со страницы оплаты
Если используется страница оплаты, покупатель приходит на адрес возврата или отмены с параметрами 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}, а не из параметра. Товар по редиректу не отдаётся.
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"));
}
Шаг 8. Выплаты — если нужны
POST /v1/payouts — см. выплаты:
- платить можно только на адреса из вайтлиста мерчанта. Адреса вносит оператор, новый адрес становится доступен через задержку, по умолчанию 24 часа. Адрес для выплаты никогда не берётся из истории транзакций;
reference— ключ идемпотентности: повтор с тем жеreferenceвозвращает ту же выплату;pending_review— не ошибка: выплата принята и ждёт решения человека;503 payouts_halted— повторить позже с тем жеreference;- исход приходит вебхуком
payout.confirmedилиpayout.failed.
Шаг 9. Перед запуском
Пройдите чек-лист перед продом целиком и проверьте весь путь на небольшой сумме: инвойс, оплата, вебхук, проверка статуса, выдача товара.