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

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

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

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

1. **Ключ API** вида `sk_<16 шестнадцатеричных символов>_<43 символа base64url>`. Его выдаёт оператор STONS, показывается он один раз — см. [аутентификацию](https://stons.io/docs/authentication.md).
2. **Секрет вебхуков.** Выдаётся вместе с ключом при создании мерчанта — см. [вебхуки](https://stons.io/docs/webhooks.md#подпись).
3. **URL для вебхуков**, который оператор указал у мерчанта. Сменить его может только оператор.
4. **Режим адресов мерчанта**: `per_invoice` — новый адрес под каждый инвойс, `per_user` — постоянный адрес пользователя. Режим задаёт оператор — см. [адреса](https://stons.io/docs/addresses.md#режимы).
5. **Как показывать оплату:** своей страницей в продукте (адрес и сумма из ответа API) или страницей оплаты сервиса по ссылке `checkout_url`. Во втором случае — адреса возврата и отмены, которые оператор задал мерчанту: без них страница не вернёт покупателя на сайт — см. [страницу оплаты](https://stons.io/docs/checkout.md#адреса-возврата).
6. **Активы:** `USDT_TRON`, `USDC_TRON` или оба — см. [инвойсы](https://stons.io/docs/invoices.md#активы).

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

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

Ключ и секрет — только на сервере продукта, в менеджере секретов или переменных окружения. Их нет в браузере, мобильном приложении, репозитории и логах — см. [чек-лист](https://stons.io/docs/checklist.md#ключи-и-доступ). Имена переменных выбирает проект, например:

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

Адрес API — с `https://`: по `http://` ключ ушёл бы открытым текстом ещё до перенаправления — см. [аутентификацию](https://stons.io/docs/authentication.md#сеть).

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

- Базовый адрес — `https://api.stons.io/v1`, ключ — в заголовке `X-Api-Key`, тела запросов и ответов — JSON.
- **Суммы — строки** в единицах актива: `"49.9"` в запросе, `"49.900000"` в ответе. Не превращайте их в числа с плавающей точкой — см. [инвойсы](https://stons.io/docs/invoices.md#суммы).
- Ошибка — тело `{"error": {"code", "message", "details"}}`. Решения принимайте по `error.code`, а не по тексту `message` — см. [коды ошибок](https://stons.io/docs/errors.md).
- `429 rate_limited` — повторить через время из заголовка `Retry-After`.
- `X-Request-Id` из ответа сохраняйте в логах: по нему поддержка находит запрос.
- Описание API в формате OpenAPI 3.1 — `https://api.stons.io/v1/openapi.json`.

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

`POST /v1/invoices` — см. [создание инвойса](https://stons.io/docs/invoices.md#создать-инвойс).

```sh
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` — **целиком**, без сокращений вида «начало…конец»: на сокращённые адреса охотятся подменой похожим адресом — см. [адреса](https://stons.io/docs/addresses.md#показ-адреса);
- `amount_expected` и актив. Это сумма к оплате: если в проекте комиссию сервиса платит покупатель, она больше цены заказа, а сама цена — в `amount_requested`. Свою цену сверяйте с `amount_requested` — см. [комиссии](https://stons.io/docs/invoices.md#комиссии);
- сеть — TRON, с предупреждением, что перевод другого токена или в другой сети не зачисляется автоматически — см. [чек-лист](https://stons.io/docs/checklist.md#отображение).

Статус узнавайте из вебхуков (шаг 5) и запросом `GET /v1/invoices/{id}`.

## Шаг 4Б. Страница оплаты сервиса

Отправьте покупателя по `checkout_url` из ответа. Если поле `null`, оператор не настроил страницу — используйте шаг 4А или обратитесь к оператору. Страница сама показывает сумму, адрес, QR-код и статус, а когда у инвойса появляется исход, возвращает покупателя на адрес возврата или отмены — см. [страницу оплаты](https://stons.io/docs/checkout.md).

Для отдельного инвойса адреса можно переопределить полями `return_url` и `cancel_url` в `POST /v1/invoices`, но только с тем же origin, что у адресов мерчанта, иначе `422 redirect_url_not_allowed`.

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

Сервис шлёт `POST` на URL мерчанта. Обработчик — см. [вебхуки](https://stons.io/docs/webhooks.md):

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 — из [вебхуков](https://stons.io/docs/webhooks.md#подпись):

```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}` — см. [статусы](https://stons.io/docs/invoices.md#статусы):

| Статус | Что делать |
| --- | --- |
| `created`, `seen` | Ждать. `seen` — перевод замечен, но ещё может откатиться |
| `confirmed`, `settled` | Отдать товар. Переплата — тоже `confirmed`, `amount_received` больше `amount_expected` |
| `underpaid` | Срок вышел, пришла часть суммы: решает продукт |
| `expired` | Срок вышел без платежа: закрыть заказ |
| `paid_late` | Платёж пришёл после срока: решает продукт |

Выдачу товара делайте идемпотентной: повтор вебхука или редиректа не должен выдать товар второй раз.

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

Если используется страница оплаты, покупатель приходит на адрес возврата или отмены с параметрами `invoice_id`, `order_id`, `status`, `ts` и `signature`. Обработчик — см. [подпись редиректа](https://stons.io/docs/checkout.md#подпись):

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

```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"));
}
```

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

`POST /v1/payouts` — см. [выплаты](https://stons.io/docs/payouts.md):

- платить можно только на адреса из вайтлиста мерчанта. Адреса вносит оператор, новый адрес становится доступен через задержку, по умолчанию 24 часа. Адрес для выплаты никогда не берётся из истории транзакций;
- `reference` — ключ идемпотентности: повтор с тем же `reference` возвращает ту же выплату;
- `pending_review` — не ошибка: выплата принята и ждёт решения человека;
- `503 payouts_halted` — повторить позже с тем же `reference`;
- исход приходит вебхуком `payout.confirmed` или `payout.failed`.

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

Пройдите [чек-лист перед продом](https://stons.io/docs/checklist.md) целиком и проверьте весь путь на небольшой сумме: инвойс, оплата, вебхук, проверка статуса, выдача товара.
