Перейти к содержанию
STONS
Разделы документации

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

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

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

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

  1. Ключ API вида sk_<16 шестнадцатеричных символов>_<43 символа base64url>. Его выдаёт оператор STONS, показывается он один раз — см. аутентификацию.
  2. Секрет вебхуков. Выдаётся вместе с ключом при создании мерчанта — см. вебхуки.
  3. URL для вебхуков, который оператор указал у мерчанта. Сменить его может только оператор.
  4. Режим адресов мерчанта: per_invoice — новый адрес под каждый инвойс, per_user — постоянный адрес пользователя. Режим задаёт оператор — см. адреса.
  5. Как показывать оплату: своей страницей в продукте (адрес и сумма из ответа API) или страницей оплаты сервиса по ссылке checkout_url. Во втором случае — адреса возврата и отмены, которые оператор задал мерчанту: без них страница не вернёт покупателя на сайт — см. страницу оплаты.
  6. Активы: 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 мерчанта. Обработчик — см. вебхуки:

  1. Читает сырое тело запроса, до разбора JSON.
  2. Проверяет подпись: X-Signature: sha256=<hex> — это HMAC-SHA256 секретом вебхуков по строке <X-Timestamp>.<сырое тело>. Отклоняет вебхук, если X-Timestamp отличается от текущего времени больше чем на 5 минут. Сравнивает подписи за постоянное время.
  3. Отбрасывает дубли по id события: одно событие может прийти несколько раз.
  4. Отвечает 2xx сразу после проверки, а работу выполняет асинхронно: вебхук без ответа 2xx за 10 секунд доставляется повторно.
  5. Не доверяет телу. По 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. Обработчик — см. подпись редиректа:

  1. Проверяет подпись: HMAC-SHA256 секретом вебхуков по строкам redirect, ts, invoice_id, status, order_id, соединённым символом \n, и окно ts в 5 минут.
  2. Проверяет, что invoice_id — инвойс этого order_id.
  3. Берёт статус из 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. Перед запуском

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