> ## Documentation Index
> Fetch the complete documentation index at: https://wallet-docs.iexexchanger.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Счета — выставление платёжной ссылки

> Создать счёт по API: клиент получает hosted-страницу оплаты с адресом, QR, обратным отсчётом и статусом в реальном времени

Счёт (invoice) — это **готовая платёжная страница** (hosted payment page), которую размещает ваш
кошелёк. CMS создаёт счёт одним запросом, получает публичную ссылку и редиректит на неё клиента.
Клиент видит сумму, адрес, QR-код, обратный отсчёт и **статус оплаты в реальном времени** — вам не
нужно верстать платёжный экран самостоятельно.

Под капотом счёт — это обычный депозит: адрес выдаётся тем же депозитным путём со всеми денежными
проверками (уникальный адрес на заявку, подтверждения, идемпотентность, переплата/недоплата).
Счёт лишь добавляет публичный неугадываемый токен и срок жизни ссылки.

<Info>
  Создание счёта (`POST`) подписывается HMAC, как все write-операции — см. [Аутентификация](/authentication).
  А вот публичные `GET`-эндпоинты страницы оплаты **не требуют подписи**: токен в URL и есть
  аутентификация (страницу открывает конечный клиент, а не ваш сервер). Ответы — всегда в конверте
  `{ "ok": true, "data": … }`.
</Info>

## Эндпоинты

| Метод  | Путь                                 | Назначение                          | Аутентификация    |
| ------ | ------------------------------------ | ----------------------------------- | ----------------- |
| `POST` | `/v1/public/invoices`                | создать счёт (ссылку оплаты)        | HMAC-подпись      |
| `GET`  | `/v1/public/invoices/{token}`        | данные платёжной страницы по токену | публичный (токен) |
| `GET`  | `/v1/public/invoices/{token}/status` | облегчённый статус для поллинга     | публичный (токен) |

## Как это работает

<Steps>
  <Step title="CMS создаёт счёт">
    `POST /v1/public/invoices` с суммой и активом. В ответ — `token` и `payPath`.
  </Step>

  <Step title="Редирект клиента на оплату">
    Соберите абсолютную ссылку `https://<домен-вашего-кошелька>{payPath}` и откройте её клиенту.
    Страница сама покажет адрес, QR, сумму и обратный отсчёт.
  </Step>

  <Step title="Клиент платит, платформа отслеживает">
    Приход средств отслеживается существующим мониторингом депозитов ровно до конца срока счёта.
  </Step>

  <Step title="Вы узнаёте об оплате">
    Либо вебхуком `deposit.finalized` на ваш `callback_url` (рекомендуется), либо поллингом статуса
    по токену. См. [Отслеживание оплаты](#отслеживание-оплаты).
  </Step>
</Steps>

***

## Создать счёт

`POST /v1/public/invoices`

Создаёт счёт для вызывающего сайта и возвращает данные, нужные для редиректа клиента на оплату.

### Тело запроса

<ParamField body="assetCode" type="string" required>
  Код актива: `USDT_TRC20`, `TRX`, `GRAM`, `USDT_TON`, … (до 32 символов). Список доступных активов —
  `GET /v1/public/assets` (см. [Валюты](/currencies)).
</ParamField>

<ParamField body="amount" type="string" required>
  Сумма к оплате — **десятичная строка**, строго больше нуля (например `"49.90"`). Никогда не число:
  деньги передаются строками, чтобы избежать потери точности. `"0"` и отрицательные значения отвергаются
  с `INVALID_AMOUNT`.
</ParamField>

<ParamField body="ttlMinutes" type="integer" default="60">
  Срок действия счёта в минутах. Диапазон **5…43200** (до 30 дней), по умолчанию **60**. Срок задаёт
  окно мониторинга: платформа следит за адресом ровно до `expiresAt`. После истечения счёт переходит в
  `expired`, но **публичная ссылка живёт ещё 1 день** (grace), чтобы клиент увидел финальный статус —
  затем ссылка отключается (`410 Gone`).
</ParamField>

<ParamField body="orderId" type="string">
  Ваш `order_id`. Если не передан — платформа сгенерирует уникальный. Уникален в рамках сайта; повтор
  того же `order_id` идемпотентно вернёт **тот же** счёт (второй не создаётся). До 255 символов.
  См. [Идемпотентность](/idempotency).
</ParamField>

<ParamField body="title" type="string">
  Заголовок/назначение платежа — показывается клиенту на странице оплаты (до 200 символов).
</ParamField>

<ParamField body="description" type="string">
  Описание — показывается клиенту на странице оплаты (до 1000 символов).
</ParamField>

<ParamField body="sweepDestinationWalletUuid" type="string">
  UUID системного кошелька-получателя свипа после оплаты (override маршрутизации). Если не указан —
  применяется дефолтная маршрутизация (обычно hot). Кошелёк должен быть `active`, той же сети, что и
  актив, и иметь роль `hot`, `admin`, `payout` или `cold` (роль `energy` запрещена).
</ParamField>

<RequestExample>
  <CodeGroup>
    ```bash cURL theme={null}
    curl -X POST https://wallet.your-exchange.com/v1/public/invoices \
      -H "X-Api-Id: pk_live_a1b2c3d4" \
      -H "X-Timestamp: 1782345600" \
      -H "X-Signature: 9b1f8c2d4e6a0f3b5c7d9e1a2b4c6d8e0f1a3b5c7d9e1f2a4b6c8d0e2f4a6b8c" \
      -H "X-Idempotency-Key: 3f5a8c1e-2d4b-4f6a-8c0e-1b3d5f7a9c2e" \
      -H "Content-Type: application/json" \
      -d '{
        "assetCode": "USDT_TRC20",
        "amount": "49.90",
        "ttlMinutes": 60,
        "orderId": "order-1024",
        "title": "Оплата заказа №1024"
      }'
    ```

    ```javascript Node.js theme={null}
    import crypto from "node:crypto";

    const baseUrl   = "https://wallet.your-exchange.com";
    const apiId     = "pk_live_a1b2c3d4";
    const apiSecret = process.env.WALLET_API_SECRET; // никогда не уходит по сети

    const body = JSON.stringify({
      assetCode: "USDT_TRC20",
      amount: "49.90",
      ttlMinutes: 60,
      orderId: "order-1024",
      title: "Оплата заказа №1024",
    });

    const timestamp = Math.floor(Date.now() / 1000).toString();
    const signature = crypto
      .createHmac("sha256", apiSecret)
      .update(`${timestamp}.${body}`)
      .digest("hex");

    const res = await fetch(`${baseUrl}/v1/public/invoices`, {
      method: "POST",
      headers: {
        "X-Api-Id": apiId,
        "X-Timestamp": timestamp,
        "X-Signature": signature,
        "X-Idempotency-Key": crypto.randomUUID(),
        "Content-Type": "application/json",
      },
      body,
    });

    const { ok, data, error } = await res.json();
    if (!ok) throw new Error(`${error.code}: ${error.message}`);

    // Абсолютная ссылка для редиректа клиента:
    const payUrl = `${baseUrl}${data.payPath}`;
    console.log(payUrl); // https://wallet.your-exchange.com/invoice/1c06b6b4...
    ```
  </CodeGroup>
</RequestExample>

<ResponseExample>
  ```json 201 Created theme={null}
  {
    "ok": true,
    "data": {
      "uuid": "8f3a1c2e-5b6d-4e7f-9a0b-1c2d3e4f5a6b",
      "orderId": "order-1024",
      "token": "1c06b6b4f6dbb5aaf795b27757a3d2c9d1e0f2a3b4c5d6e7f8091a2b3c4d5e6f",
      "payPath": "/invoice/1c06b6b4f6dbb5aaf795b27757a3d2c9d1e0f2a3b4c5d6e7f8091a2b3c4d5e6f",
      "assetCode": "USDT_TRC20",
      "network": "TRON",
      "address": "TKh9...c4dZ",
      "memo": null,
      "amount": "49.90",
      "expiresAt": "2026-06-25T14:00:00.000Z",
      "status": "pending"
    }
  }
  ```
</ResponseExample>

### Поля ответа

<ResponseField name="uuid" type="string">Внутренний UUID счёта (для корреляции).</ResponseField>
<ResponseField name="orderId" type="string">Ваш `order_id` (или сгенерированный, если не передавали).</ResponseField>
<ResponseField name="token" type="string">Публичный токен ссылки (64 hex-символа, неугадываемый). Используется в публичных `GET`-эндпоинтах и в `payPath`.</ResponseField>
<ResponseField name="payPath" type="string">Относительный путь страницы оплаты. Абсолютная ссылка: `https://<домен-вашего-кошелька>{payPath}`.</ResponseField>
<ResponseField name="assetCode" type="string">Код актива счёта.</ResponseField>
<ResponseField name="network" type="string">Сеть актива (`TRON`, `TON`, …).</ResponseField>
<ResponseField name="address" type="string">Адрес для оплаты (тот же, что покажет страница).</ResponseField>
<ResponseField name="memo" type="string | null">Memo для memo-сетей (TON). Для остальных сетей — `null`.</ResponseField>
<ResponseField name="amount" type="string">Сумма к оплате (десятичная строка).</ResponseField>
<ResponseField name="expiresAt" type="string">Срок действия счёта (ISO-8601). После него счёт `expired`.</ResponseField>
<ResponseField name="status" type="string">Состояние счёта на момент создания — обычно `pending`. Полный набор — [Состояния счёта](#состояния-счёта).</ResponseField>

<Warning>
  Для memo-сетей (TON) `memo` **обязателен** при оплате. Hosted-страница показывает его отдельно с
  предупреждением, но если вы строите свой UI поверх API — обязательно выводите `memo` рядом с адресом:
  перевод без memo не будет привязан к счёту.
</Warning>

***

## Отслеживание оплаты

Есть два способа узнать об оплате. Их можно комбинировать.

<Steps>
  <Step title="Вебхук (рекомендуется)">
    Поскольку счёт — это депозит, при финализации оплаты платформа отправит на `callback_url` вашего
    сайта событие **`deposit.finalized`** с вашим `order_id`. Отдельной подписки не нужно — достаточно
    настроенного `callback_url`. См. [Webhooks](/webhooks/overview).
  </Step>

  <Step title="Поллинг по токену">
    Опрашивайте `GET /v1/public/invoices/{token}/status` (облегчённый ответ) каждые \~3 секунды для
    live-обновления экрана оплаты. Полное состояние страницы — `GET /v1/public/invoices/{token}`.
    Оба эндпоинта **публичные** (подпись не нужна, аутентификация по токену).
  </Step>
</Steps>

### Получить данные платёжной страницы

`GET /v1/public/invoices/{token}`

Возвращает всё, что нужно для отрисовки страницы оплаты: адрес, актив, сумму, USD-эквивалент (best-effort),
срок (для обратного отсчёта), текущее состояние, подтверждения, ссылки на explorer и брендинг оператора.

<RequestExample>
  ```bash cURL theme={null}
  curl https://wallet.your-exchange.com/v1/public/invoices/1c06b6b4f6dbb5aaf795b27757a3d2c9d1e0f2a3b4c5d6e7f8091a2b3c4d5e6f
  ```
</RequestExample>

<ResponseExample>
  ```json 200 OK (ожидает оплату) theme={null}
  {
    "ok": true,
    "data": {
      "token": "1c06b6b4f6dbb5aaf795b27757a3d2c9d1e0f2a3b4c5d6e7f8091a2b3c4d5e6f",
      "title": "Оплата заказа №1024",
      "description": null,
      "assetCode": "USDT_TRC20",
      "assetSymbol": "USDT",
      "assetName": "Tether USD (TRC-20)",
      "network": "TRON",
      "address": "TKh9...c4dZ",
      "memo": null,
      "expectedAmount": "49.90",
      "amountUsd": "49.90",
      "decimals": 6,
      "expiresAt": "2026-06-25T14:00:00.000Z",
      "createdAt": "2026-06-25T13:00:00.000Z",
      "state": "pending",
      "paymentStatus": "check",
      "receivedAmount": null,
      "confirmations": null,
      "requiredConfirmations": 19,
      "txhash": null,
      "explorerTxUrl": null,
      "explorerAddressUrl": "https://tronscan.org/#/address/TKh9...c4dZ",
      "paidAt": null,
      "branding": {
        "displayName": "WalletCore",
        "primaryColor": "#3b82f6"
      }
    }
  }
  ```
</ResponseExample>

#### Поля ответа

<ResponseField name="token" type="string">Публичный токен счёта.</ResponseField>
<ResponseField name="title" type="string | null">Заголовок/назначение платежа.</ResponseField>
<ResponseField name="description" type="string | null">Описание счёта.</ResponseField>
<ResponseField name="assetCode" type="string">Код актива (`USDT_TRC20`).</ResponseField>
<ResponseField name="assetSymbol" type="string">Короткий символ для UI (`USDT`).</ResponseField>
<ResponseField name="assetName" type="string">Человекочитаемое имя актива.</ResponseField>
<ResponseField name="network" type="string">Сеть актива.</ResponseField>
<ResponseField name="address" type="string">Адрес для оплаты (показывается + QR).</ResponseField>
<ResponseField name="memo" type="string | null">Memo для memo-сетей (обязателен при оплате), иначе `null`.</ResponseField>
<ResponseField name="expectedAmount" type="string">Сумма к оплате.</ResponseField>
<ResponseField name="amountUsd" type="string | null">USD-эквивалент суммы (best-effort, по oracle). `null`, если цены нет.</ResponseField>
<ResponseField name="decimals" type="number">Число знаков после запятой для актива.</ResponseField>
<ResponseField name="expiresAt" type="string">Срок оплаты (для обратного отсчёта).</ResponseField>
<ResponseField name="createdAt" type="string">Когда счёт создан.</ResponseField>
<ResponseField name="state" type="string">Состояние оплаты для UI. См. [Состояния счёта](#состояния-счёта).</ResponseField>
<ResponseField name="paymentStatus" type="string">Сырой статус депозита (для отладки/детализации). По умолчанию `check`.</ResponseField>
<ResponseField name="receivedAmount" type="string | null">Фактически полученная сумма (`null`, пока нет tx).</ResponseField>
<ResponseField name="confirmations" type="number | null">Текущее число подтверждений (`null`, пока нет tx).</ResponseField>
<ResponseField name="requiredConfirmations" type="number">Сколько подтверждений нужно для финализации.</ResponseField>
<ResponseField name="txhash" type="string | null">Хеш входящей транзакции.</ResponseField>
<ResponseField name="explorerTxUrl" type="string | null">Ссылка на tx в explorer.</ResponseField>
<ResponseField name="explorerAddressUrl" type="string | null">Ссылка на адрес в explorer.</ResponseField>
<ResponseField name="paidAt" type="string | null">Когда оплата финализирована (`null`, пока не оплачено).</ResponseField>

<ResponseField name="branding" type="object">
  Брендинг оператора для white-label страницы.

  <Expandable title="поля branding">
    <ResponseField name="displayName" type="string">Отображаемое имя оператора.</ResponseField>
    <ResponseField name="primaryColor" type="string">Основной цвет (hex).</ResponseField>
  </Expandable>
</ResponseField>

### Опрашивать статус оплаты

`GET /v1/public/invoices/{token}/status`

Облегчённый ответ для частого поллинга страницы (раз в \~3 секунды). Содержит только поля, нужные для
live-обновления, без брендинга и метаданных актива.

<RequestExample>
  ```bash cURL theme={null}
  curl https://wallet.your-exchange.com/v1/public/invoices/1c06b6b4f6dbb5aaf795b27757a3d2c9d1e0f2a3b4c5d6e7f8091a2b3c4d5e6f/status
  ```
</RequestExample>

<ResponseExample>
  ```json 200 OK (оплата замечена, идут подтверждения) theme={null}
  {
    "ok": true,
    "data": {
      "state": "detected",
      "paymentStatus": "process",
      "receivedAmount": "49.90",
      "confirmations": 7,
      "requiredConfirmations": 19,
      "txhash": "a1b2c3d4e5f6...90ab",
      "explorerTxUrl": "https://tronscan.org/#/transaction/a1b2c3d4e5f6...90ab",
      "paidAt": null,
      "expiresAt": "2026-06-25T14:00:00.000Z"
    }
  }
  ```
</ResponseExample>

#### Поля ответа

<ResponseField name="state" type="string">Состояние оплаты для UI. См. [Состояния счёта](#состояния-счёта).</ResponseField>
<ResponseField name="paymentStatus" type="string">Сырой статус депозита.</ResponseField>
<ResponseField name="receivedAmount" type="string | null">Фактически полученная сумма.</ResponseField>
<ResponseField name="confirmations" type="number | null">Текущее число подтверждений.</ResponseField>
<ResponseField name="requiredConfirmations" type="number">Сколько нужно для финализации.</ResponseField>
<ResponseField name="txhash" type="string | null">Хеш входящей транзакции.</ResponseField>
<ResponseField name="explorerTxUrl" type="string | null">Ссылка на tx в explorer.</ResponseField>
<ResponseField name="paidAt" type="string | null">Когда оплата финализирована.</ResponseField>
<ResponseField name="expiresAt" type="string">Срок оплаты (для обратного отсчёта).</ResponseField>

<Tip>
  Прогресс подтверждений считайте как `confirmations` / `requiredConfirmations` (например `7 / 19`).
  Сравнивайте `receivedAmount` с `expectedAmount`, чтобы отличить точную оплату (`paid`) от переплаты
  (`overpaid`) и недоплаты (`underpaid`).
</Tip>

***

## Состояния счёта

Поле `state` (в обоих GET-ответах) и `status` (при создании) — это упрощённое для UI представление
статуса депозита. Возможные значения:

| `state`     | Что значит                                    | Финальное |
| ----------- | --------------------------------------------- | --------- |
| `pending`   | адрес выдан, оплаты ещё нет, срок не истёк    | нет       |
| `detected`  | входящая tx замечена, идут подтверждения      | нет       |
| `paid`      | оплачено точно (сумма совпала)                | да        |
| `overpaid`  | переплата (получено больше ожидаемого)        | да        |
| `underpaid` | недоплата (получено меньше ожидаемого)        | да        |
| `refunded`  | оплата возвращена отправителю                 | да        |
| `expired`   | срок истёк, оплаты не было (или счёт отменён) | да        |

<Note>
  Счёт делегирует приём средств депозиту, поэтому переплата/недоплата обрабатываются ровно как у
  депозита. Логика подтверждений и финализации — та же; см. [Статусы депозита](/deposits/statuses).
  Поле `paymentStatus` отдаёт сырой статус депозита, если нужна более точная детализация.
</Note>

***

## Краевые случаи и ошибки

| Ситуация                                           | Поведение                                                       |
| -------------------------------------------------- | --------------------------------------------------------------- |
| Сумма `"0"` или отрицательная                      | `422 INVALID_AMOUNT` («Amount must be greater than zero»)       |
| `ttlMinutes` вне диапазона 5…43200                 | `422 VALIDATION_FAILED`                                         |
| Неизвестный `assetCode`                            | `404 NOT_FOUND` / `INVALID_ASSET`                               |
| Повтор того же `orderId`                           | идемпотентно возвращается **тот же** счёт (второй не создаётся) |
| `sweepDestinationWalletUuid` не найден / неактивен | `422 VALIDATION_FAILED`                                         |
| Кошелёк свипа в другой сети, чем актив             | `422 VALIDATION_FAILED`                                         |
| Роль кошелька свипа не из `hot/admin/payout/cold`  | `422 VALIDATION_FAILED`                                         |
| Запрос токена несуществующего счёта                | `404 NOT_FOUND`                                                 |
| Ссылка отключена оператором                        | `410 Gone`                                                      |
| Прошёл срок счёта + 1 день (grace)                 | `410 Gone` — страница больше недоступна                         |

Полный формат ошибки и справочник кодов — [Ошибки](/errors).

***

## Частые вопросы

<AccordionGroup>
  <Accordion title="Чем счёт отличается от депозита?">
    Ничем по сути приёма средств — счёт это депозит плюс публичная hosted-страница оплаты и срок жизни
    ссылки. Если вам не нужна готовая страница (вы рисуете платёжный экран сами), используйте
    [Депозиты](/deposits/overview) напрямую. Если хотите редиректить клиента на готовую страницу —
    используйте счета.
  </Accordion>

  <Accordion title="Нужно ли подписывать запросы к странице оплаты?">
    Нет. `POST` на создание счёта подписывается HMAC (это делает ваш сервер). А `GET /{token}` и
    `GET /{token}/status` — публичные: их вызывает браузер клиента, и аутентификацией служит сам
    неугадываемый токен в URL. Не встраивайте `api_secret` в клиентский код.
  </Accordion>

  <Accordion title="Как клиент попадает на оплату?">
    Из ответа на создание возьмите `payPath` и соберите абсолютную ссылку
    `https://<домен-вашего-кошелька>{payPath}`. Откройте её клиенту (редирект или новая вкладка).
    Альтернативно постройте ссылку из `token`: `…/invoice/{token}`.
  </Accordion>

  <Accordion title="Как долго работает ссылка?">
    Платформа мониторит адрес до `expiresAt` (задаётся `ttlMinutes`, по умолчанию 60 минут). После
    истечения счёт переходит в `expired`, но публичная ссылка остаётся доступной ещё **1 день**, чтобы
    клиент увидел финальный статус. Затем ссылка отключается и возвращает `410 Gone`. Оператор также
    может отключить ссылку вручную из админки в любой момент.
  </Accordion>

  <Accordion title="Лучше вебхук или поллинг?">
    Вебхук `deposit.finalized` — основной и надёжный способ обновить вашу заявку на бэкенде (см.
    [Webhooks](/webhooks/overview)). Поллинг `GET /{token}/status` — для живого обновления экрана у
    клиента в браузере (раз в \~3 секунды). Обычно используют оба: вебхук на сервере, поллинг на странице.
  </Accordion>

  <Accordion title="Что с переплатой и недоплатой?">
    Обрабатываются как у депозита: точная сумма → `paid`, больше → `overpaid`, меньше → `underpaid`.
    Сравнивайте `receivedAmount` и `expectedAmount`. Окно мониторинга равно сроку счёта — после
    `expiresAt` адрес больше не отслеживается.
  </Accordion>

  <Accordion title="Можно ли направить средства на конкретный кошелёк?">
    Да. Передайте `sweepDestinationWalletUuid` при создании — после оплаты свип уйдёт на этот системный
    кошелёк (override маршрутизации). Кошелёк должен быть `active`, той же сети, что и актив, и иметь роль
    `hot`, `admin`, `payout` или `cold`. Без этого поля применяется дефолтная маршрутизация.
  </Accordion>
</AccordionGroup>
