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

# Vito API и покупки

> Разрешите одобренным приложениям списывать ваш Vito с подтверждением PIN и управляйте запросами на оплату со страницы Purchases.

<Info>
  Требуется подписка **Silver** или выше — как для подачи заявки, так и для каждого авторизованного вызова. Если подписка владельца ключа истекает, проект замораживается до её возобновления.
</Info>

REST API, позволяющий вашему приложению работать с балансом [Vito](/ru/members/vito) пользователя прямо из Discord: читать его, списывать, начислять или переводить между пользователями. Все эндпоинты возвращают JSON и версионированы под `/v1`.

<Warning>
  **Vito никогда не переходит в реальные деньги и не берётся из них.** Он перемещается только между балансами Vetox.
</Warning>

## Получение доступа

Доступ выдаётся **на каждый проект отдельно**. Нужны все четыре условия:

<Steps>
  <Step title="Активная подписка, Silver или выше">
    Проверяется при каждом вызове, а не только при одобрении.
  </Step>

  <Step title="Одобренная заявка разработчика">
    Отправляется со страницы Vito API в панели управления. Рассматривается командой Vetox вручную.
  </Step>

  <Step title="Принятые условия для разработчиков API">
    Подтверждаются при отправке заявки.
  </Step>

  <Step title="Scopes, необходимые вашему проекту">
    Выдаются командой Vetox исходя из вашего описания.
  </Step>
</Steps>

<Tip>
  Пишите конкретно, что вы создаёте и как будете хранить ключ. Отклоняют именно расплывчатые заявки.
</Tip>

### Scopes

| Scope               | Разрешает                                                       |
| ------------------- | --------------------------------------------------------------- |
| `balance:read`      | Читать баланс Vito пользователя                                 |
| `deduct:create`     | Списывать с баланса пользователя                                |
| `credit:create`     | Начислять Vito пользователю за счёт вашего собственного баланса |
| `transfer:create`   | Переводить Vito между двумя пользователями                      |
| `transactions:read` | Просматривать и читать транзакции вашего проекта                |

## Аутентификация

Передавайте секретный ключ как Bearer-токен:

```bash theme={null}
Authorization: Bearer vito_live_xxxxxxxxxxxxxxxx
```

Два необязательных уровня дополнительно защищают проект:

* **Список разрешённых IP** — ограничивает вызовы конкретными IP-адресами серверов
* **Лимиты частоты** — потолки на проект, растущие вместе с уровнем подписки владельца

### Ключи, ротация и хранение

<Warning>
  **Ключ и секрет подписи показываются ровно один раз.** После одобрения у вас есть **окно в 7 дней**, чтобы раскрыть их на вкладке API-ключей. Vetox хранит только хеш и не может показать их снова — если пропустите окно, придётся выполнить ротацию.
</Warning>

* Держите ключ **только на сервере** — кто им владеет, может списывать средства ваших пользователей
* Выполняйте ротацию на вкладке API-ключей. Прежний ключ продолжает работать **24 часа** — льготный период, чтобы выкатить обновление без простоя
* Секрет подписи вебхуков ротируется отдельно, со своим 24-часовым перекрытием
* При утечке немедленно выполните ротацию

## Списание с пользователя

<Warning>
  **Один только ключ не может переместить Vito пользователя.** Каждое списание требует подтверждения пользователем PIN-кодом его кошелька, на `vetox.io` — никогда внутри вашего приложения и никогда внутри Discord.
</Warning>

<Steps>
  <Step title="Ваше приложение вызывает POST /v1/deduct">
    С пользователем, суммой, исходным `guildId` и данными товара.
  </Step>

  <Step title="Vito возвращает confirmUrl">
    Ожидающее подтверждение, действительное **10 минут**. Пользователю также приходит ЛС.
  </Step>

  <Step title="Пользователь подтверждает PIN-кодом">
    На `vetox.io`.
  </Step>

  <Step title="Vito проводит расчёт и уведомляет">
    Баланс списывается, транзакция записывается, и отправляется подписанный вебхук, если он у вас настроен.
  </Step>

  <Step title="Ваше приложение проверяет и завершает">
    Проверьте подпись, затем откройте доступ к контенту или выдайте товар.
  </Step>
</Steps>

<Warning>
  **Завершайте своё действие только по `confirmation.completed`** — никогда по ответу `/deduct`. На этом этапе списание ещё не окончательное.
</Warning>

### Параметры запроса — `/v1/deduct`

| Поле          | Обязательно | Примечания                                                                                        |
| ------------- | ----------- | ------------------------------------------------------------------------------------------------- |
| `discordId`   | **Да**      | Discord ID пользователя — snowflake из 17–20 цифр                                                 |
| `amount`      | **Да**      | Положительное целое число                                                                         |
| `guildId`     | **Да**      | Discord-сервер, с которого исходит списание. Любое списание должно исходить с сервера             |
| `reason`      | Нет         | До 256 символов. Показывается пользователю и возвращается в вебхуке                               |
| `merchantRef` | Нет         | Ваша собственная ссылка, до 128 символов. Используйте для сопоставления вебхука с вашими записями |
| `product`     | Нет         | `{ type, name, description?, imageUrl? }` — отображается на странице подтверждения и в ЛС         |
| `imageUrl`    | Нет         | Должен начинаться с `https://`                                                                    |
| `metadata`    | Нет         | До **10** строковых пар ключ/значение, передаются без изменений                                   |

## Начисление пользователю

`POST /v1/add` начисляет Vito пользователю **из вашего собственного баланса** — для наград или возвратов. Те же поля, что и у `/deduct`, кроме `guildId` и `product`.

<Note>
  В отличие от списания, начисление **не имеет шага подтверждения** — расчёт происходит сразу. Требуется scope `credit:create` и достаточный баланс, иначе вызов вернёт **402 `VITO_INSUFFICIENT_OWNER_FUNDS`**.
</Note>

## Комиссии

Каждое списание поступает вам за вычетом комиссии платформы — по той же шкале, что и переводы Vito внутри приложения, в зависимости от **вашего** уровня подписки:

| Ваша подписка        | Комиссия |
| -------------------- | -------- |
| Normal, Silver, Gold | 7%       |
| Platinum             | 6%       |
| Diamond              | 5%       |

<Note>
  Суммы **5 Vito и меньше не облагаются комиссией**, а начисления через `/v1/add` не облагаются ею никогда.
</Note>

## Вебхуки

Добавьте один или несколько `https`-адресов обратного вызова на вкладке настроек. Vito отправляет подписанный `POST`, как только подтверждение достигает конечного состояния.

<Warning>
  Вебхуки отправляются, только если у проекта есть **и** адрес обратного вызова, **и** секрет подписи. Раскройте секрет (`whsec_…`) один раз на вкладке API-ключей.
</Warning>

### Проверка подписи

Каждая доставка содержит заголовок `X-Vito-Signature`:

```text theme={null}
X-Vito-Signature: ts=<unix>;h1=<hex>
```

Каждую доставку сопровождают ещё два заголовка — используйте `X-Vito-Event-Id` как ключ дедупликации, поскольку повторная попытка отправляет тот же идентификатор:

| Заголовок           | Содержит                                                        |
| ------------------- | --------------------------------------------------------------- |
| `X-Vito-Event-Id`   | Постоянный идентификатор события — одинаковый при всех повторах |
| `X-Vito-Event-Type` | Например, `confirmation.completed`                              |

<Warning>
  **Во время ротации секрета подписи заголовок содержит несколько подписей**, самая новая первой:

  ```text theme={null}
  X-Vito-Signature: ts=<unix>;h1=<новая>;h1=<старая>
  ```

  Принимайте доставку, если совпадает **любая** из `h1`. Проверяющий код, читающий только первую, будет отвергать все вебхуки, пока не выкатит новый секрет, — а это сводит на нет весь смысл 24-часового перекрытия.
</Warning>

Пересчитайте HMAC по `<ts>:<rawBody>` вашим секретом подписи и сравните за постоянное время.

```js theme={null}
const crypto = require('crypto');

function verify(rawBody, header, secret) {
  const parts = header.split(';');
  const ts = parts.find(p => p.startsWith('ts='))?.slice(3);
  const sigs = parts.filter(p => p.startsWith('h1=')).map(p => p.slice(3));
  if (!ts || sigs.length === 0) return false;

  // Reject anything older than ~5 minutes — replay protection.
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;

  const expected = Buffer.from(
    crypto.createHmac('sha256', secret).update(`${ts}:${rawBody}`).digest('hex'),
  );

  // Any matching signature is valid — a rotation emits several.
  return sigs.some(sig => {
    const actual = Buffer.from(sig);
    // timingSafeEqual throws when the lengths differ, so check first.
    return (
      actual.length === expected.length && crypto.timingSafeEqual(actual, expected)
    );
  });
}
```

<Warning>
  Проверяйте по **сырому телу запроса**, до любого разбора JSON или middleware, которое его перепишет.
</Warning>

### События

Пять типов событий с одинаковой структурой полезной нагрузки. Поле `data.status` содержит исход.

| Событие                  | Значение                                                                                    |
| ------------------------ | ------------------------------------------------------------------------------------------- |
| `confirmation.completed` | Подтверждено и списано. Содержит `transactionId`. **Завершайте своё действие здесь**        |
| `confirmation.failed`    | Не удалось завершить — недостаточно средств или внутренняя ошибка. Содержит `failureReason` |
| `confirmation.expired`   | Не подтверждено за 10 минут. Vito не перемещался                                            |
| `confirmation.cancelled` | Пользователь отменил. Vito не перемещался                                                   |
| `credit.completed`       | Начисление `/add` за счёт владельца проведено. Срабатывает сразу — без шага подтверждения   |

<Note>
  Вебхуки повторяются **5 раз с нарастающей задержкой**. Быстро отвечайте 2xx, а выдачу выполняйте асинхронно.
</Note>

## Коды ошибок

Каждый ответ обёрнут в конверт. Успех несёт `data`, ошибка — `error`, и никогда оба сразу:

```json theme={null}
{
  "success": false,
  "error": {
    "type": "Forbidden",
    "message": "API key lacks the required scope for this operation.",
    "code": "VITO_INSUFFICIENT_SCOPE"
  },
  "requestId": "…",
  "timestamp": 1735161600000
}
```

<Warning>
  **Каждый код имеет префикс `VITO_`.** Сравнивайте строку целиком — голый `RATE_LIMITED` или `FORBIDDEN` никогда не приходит в ответе.
</Warning>

| Код                                   | Статус | Значение                                                                      |
| ------------------------------------- | ------ | ----------------------------------------------------------------------------- |
| `VITO_VALIDATION_ERROR`               | 400    | Поле отсутствует или имеет неверный формат                                    |
| `VITO_INVALID_AMOUNT`                 | 400    | `amount` не является положительным целым числом                               |
| `VITO_AMOUNT_LIMIT_EXCEEDED`          | 400    | Превышен ваш лимит на одну транзакцию                                         |
| `VITO_DAILY_VOLUME_EXCEEDED`          | 400    | Этот вызов превысил бы ваш дневной лимит оборота                              |
| `VITO_SELF_TRANSFER`                  | 400    | Отправитель и получатель — один и тот же пользователь                         |
| `VITO_INVALID_API_KEY`                | 401    | Ключ отсутствует, некорректен, отозван или неизвестен                         |
| `VITO_INSUFFICIENT_FUNDS`             | 402    | У пользователя не хватает средств на списание                                 |
| `VITO_INSUFFICIENT_OWNER_FUNDS`       | 402    | **Вашего** баланса не хватает, чтобы профинансировать `/add`                  |
| `VITO_INSUFFICIENT_SCOPE`             | 403    | У ключа нет scope, который требует эндпоинт                                   |
| `VITO_IP_NOT_ALLOWED`                 | 403    | Вызывающий IP не в списке разрешённых                                         |
| `VITO_OWNER_MEMBERSHIP_INACTIVE`      | 403    | Подписка владельца истекла — заморожено до возобновления                      |
| `VITO_GUILD_NOT_ALLOWED`              | 403    | `guildId` отсутствует в списке разрешённых серверов проекта                   |
| `VITO_TOS_NOT_ACCEPTED`               | 403    | Условия для разработчиков не приняты либо ожидает принятия более новая версия |
| `VITO_PROJECT_FROZEN`                 | 403    | Заморожено — обычно из-за истёкшей подписки владельца                         |
| `VITO_PROJECT_SUSPENDED`              | 403    | Приостановлено командой Vetox                                                 |
| `VITO_PROJECT_BANNED`                 | 403    | Заблокировано командой Vetox                                                  |
| `VITO_USER_BLACKLISTED`               | 403    | Пользователю запрещены операции с Vito                                        |
| `VITO_ACCOUNT_LOCKED`                 | 403    | Кошелёк пользователя заблокирован после неудачных попыток ввода PIN           |
| `VITO_USER_NOT_FOUND`                 | 404    | Для этого Discord ID нет аккаунта Vito                                        |
| `VITO_CONFIRMATION_NOT_FOUND`         | 404    | Неизвестный токен подтверждения                                               |
| `VITO_IDEMPOTENCY_CONFLICT`           | 409    | Тот же `Idempotency-Key`, но другое тело запроса                              |
| `VITO_IDEMPOTENCY_IN_PROGRESS`        | 409    | Идентичный запрос ещё выполняется — повторите чуть позже                      |
| `VITO_CONFIRMATION_ALREADY_PROCESSED` | 409    | Это подтверждение уже достигло конечного состояния                            |
| `VITO_KEY_ROTATION_IN_PROGRESS`       | 409    | Ротация уже выполняется                                                       |
| `VITO_CONFIRMATION_EXPIRED`           | 410    | Десятиминутное окно истекло                                                   |
| `VITO_RATE_LIMITED`                   | 429    | Снизьте темп и повторите                                                      |
| `VITO_INTERNAL_ERROR`                 | 500    | Непредвиденный сбой на нашей стороне                                          |

## Лимиты частоты

| Подписка владельца   | В минуту | В час  |
| -------------------- | -------- | ------ |
| Нет                  | 60       | 1 000  |
| Silver или Gold      | 180      | 5 000  |
| Platinum или Diamond | 600      | 15 000 |

Также действует лимит на один IP, равный половине вашей минутной квоты, но не менее 30.

<Warning>
  Под нагрузкой **эндпоинты записи отказывают «закрыто»** — списание отклоняется, чтобы не рисковать двойным расходом. Эндпоинты чтения отказывают «открыто». Считайте отклонённую запись как «не произошло» и повторите её.
</Warning>

<Note>
  Отправляйте заголовок `Idempotency-Key`, чтобы безопасно исключать дубли при повторных попытках.
</Note>

## Эндпоинты

| Эндпоинт                        | Scope               |
| ------------------------------- | ------------------- |
| `GET /v1/auth/verify`           | любой               |
| `POST /v1/auth/rotate-key`      | любой               |
| `GET /v1/balance/:discordId`    | `balance:read`      |
| `POST /v1/deduct`               | `deduct:create`     |
| `POST /v1/add`                  | `credit:create`     |
| `POST /v1/transfer`             | `transfer:create`   |
| `GET /v1/transactions` и `/:id` | `transactions:read` |
| `GET /v1/webhooks/events`       | `transactions:read` |

## Ограничения

* Подтверждения истекают через **10 минут** — считайте неподтверждённые запросы брошенными
* `amount` должен быть положительным целым числом
* `metadata` ограничена 10 ключами
* Лимиты на транзакцию и на день задаёт команда Vetox; они отображаются только для чтения на вкладке настроек

## Чек-лист безопасности

<AccordionGroup>
  <Accordion title="Держите секреты на сервере" icon="lock">
    API-ключу и секрету подписи не место в клиентском коде. При утечке любого из них немедленно выполните ротацию.
  </Accordion>

  <Accordion title="Проверяйте каждый вебхук" icon="signature">
    Сверяйте подпись с сырым телом запроса и отклоняйте доставки старше \~5 минут.
  </Accordion>

  <Accordion title="Проводите расчёт только по completed" icon="circle-check">
    Никогда не выдавайте товар по ответу `/deduct` — списание окончательно только при `confirmation.completed`.
  </Accordion>

  <Accordion title="Минимум привилегий" icon="key">
    Запрашивайте только те scopes, которые действительно используете, и включите список разрешённых IP.
  </Accordion>
</AccordionGroup>

## Устранение неполадок

<AccordionGroup>
  <Accordion title="Каждый вызов возвращает «не авторизовано»">
    Подписка владельца истекла. Она перепроверяется при каждом вызове.
  </Accordion>

  <Accordion title="Я потерял ключ">
    Восстановить его нельзя — хранится только хеш. Выполните ротацию, чтобы получить новый.
  </Accordion>

  <Accordion title="Подписи вебхуков не проходят после ротации">
    Принимайте оба секрета в течение 24-часового перекрытия.
  </Accordion>

  <Accordion title="Списание никогда не завершается">
    Пользователь его не подтвердил. Подтверждения истекают через 10 минут.
  </Accordion>

  <Accordion title="Вебхуки не приходят">
    Проекту нужны и адрес обратного вызова, и секрет подписи. С одним из двух ничего не доставляется.
  </Accordion>

  <Accordion title="Пришло меньше Vito, чем я списал">
    Это расчётная комиссия. Используйте суммы 5 Vito и меньше, чтобы её избежать, либо закладывайте её в цену.
  </Accordion>

  <Accordion title="402 VITO_INSUFFICIENT_OWNER_FUNDS">
    `/v1/add` финансируется из вашего собственного баланса, а не создаётся из ничего. Пополните его.
  </Accordion>
</AccordionGroup>

<CardGroup cols={2}>
  <Card title="Vito" icon="coins" href="/ru/members/vito">
    Балансы, PIN-код и комиссии.
  </Card>

  <Card title="Запросы на оплату" icon="receipt" href="/ru/account/payment-requests">
    Что видит пользователь, когда вы списываете с него.
  </Card>
</CardGroup>
