Разрешите одобренным приложениям списывать ваш Vito с подтверждением PIN и управляйте запросами на оплату со страницы Purchases.
Требуется подписка Silver или выше — как для подачи заявки, так и для каждого авторизованного вызова. Если подписка владельца ключа истекает, проект замораживается до её возобновления.
REST API, позволяющий вашему приложению работать с балансом Vito пользователя прямо из Discord: читать его, списывать, начислять или переводить между пользователями. Все эндпоинты возвращают JSON и версионированы под /v1.
Vito никогда не переходит в реальные деньги и не берётся из них. Он перемещается только между балансами Vetox.
Ключ и секрет подписи показываются ровно один раз. После одобрения у вас есть окно в 7 дней, чтобы раскрыть их на вкладке API-ключей. Vetox хранит только хеш и не может показать их снова — если пропустите окно, придётся выполнить ротацию.
Держите ключ только на сервере — кто им владеет, может списывать средства ваших пользователей
Выполняйте ротацию на вкладке API-ключей. Прежний ключ продолжает работать 24 часа — льготный период, чтобы выкатить обновление без простоя
Секрет подписи вебхуков ротируется отдельно, со своим 24-часовым перекрытием
Один только ключ не может переместить Vito пользователя. Каждое списание требует подтверждения пользователем PIN-кодом его кошелька, на vetox.io — никогда внутри вашего приложения и никогда внутри Discord.
1
Ваше приложение вызывает POST /v1/deduct
С пользователем, суммой, исходным guildId и данными товара.
2
Vito возвращает confirmUrl
Ожидающее подтверждение, действительное 10 минут. Пользователю также приходит ЛС.
3
Пользователь подтверждает PIN-кодом
На vetox.io.
4
Vito проводит расчёт и уведомляет
Баланс списывается, транзакция записывается, и отправляется подписанный вебхук, если он у вас настроен.
5
Ваше приложение проверяет и завершает
Проверьте подпись, затем откройте доступ к контенту или выдайте товар.
Завершайте своё действие только по confirmation.completed — никогда по ответу /deduct. На этом этапе списание ещё не окончательное.
POST /v1/add начисляет Vito пользователю из вашего собственного баланса — для наград или возвратов. Те же поля, что и у /deduct, кроме guildId и product.
В отличие от списания, начисление не имеет шага подтверждения — расчёт происходит сразу. Требуется scope credit:create и достаточный баланс, иначе вызов вернёт 402 VITO_INSUFFICIENT_OWNER_FUNDS.
Каждое списание поступает вам за вычетом комиссии платформы — по той же шкале, что и переводы Vito внутри приложения, в зависимости от вашего уровня подписки:
Ваша подписка
Комиссия
Normal, Silver, Gold
7%
Platinum
6%
Diamond
5%
Суммы 5 Vito и меньше не облагаются комиссией, а начисления через /v1/add не облагаются ею никогда.
Добавьте один или несколько https-адресов обратного вызова на вкладке настроек. Vito отправляет подписанный POST, как только подтверждение достигает конечного состояния.
Вебхуки отправляются, только если у проекта есть и адрес обратного вызова, и секрет подписи. Раскройте секрет (whsec_…) один раз на вкладке API-ключей.
Каждая доставка содержит заголовок X-Vito-Signature:
X-Vito-Signature: ts=<unix>;h1=<hex>
Каждую доставку сопровождают ещё два заголовка — используйте X-Vito-Event-Id как ключ дедупликации, поскольку повторная попытка отправляет тот же идентификатор:
Заголовок
Содержит
X-Vito-Event-Id
Постоянный идентификатор события — одинаковый при всех повторах
X-Vito-Event-Type
Например, confirmation.completed
Во время ротации секрета подписи заголовок содержит несколько подписей, самая новая первой:
Принимайте доставку, если совпадает любая из h1. Проверяющий код, читающий только первую, будет отвергать все вебхуки, пока не выкатит новый секрет, — а это сводит на нет весь смысл 24-часового перекрытия.
Пересчитайте HMAC по <ts>:<rawBody> вашим секретом подписи и сравните за постоянное время.
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) ); });}
Проверяйте по сырому телу запроса, до любого разбора JSON или middleware, которое его перепишет.
Также действует лимит на один IP, равный половине вашей минутной квоты, но не менее 30.
Под нагрузкой эндпоинты записи отказывают «закрыто» — списание отклоняется, чтобы не рисковать двойным расходом. Эндпоинты чтения отказывают «открыто». Считайте отклонённую запись как «не произошло» и повторите её.
Отправляйте заголовок Idempotency-Key, чтобы безопасно исключать дубли при повторных попытках.