> ## 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](/uk/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="/uk/members/vito">
    Баланси, PIN-код і комісії.
  </Card>

  <Card title="Запити на оплату" icon="receipt" href="/uk/account/payment-requests">
    Що бачить користувач, коли ви списуєте з нього.
  </Card>
</CardGroup>
