Overview
The Vito API lets your application work with a user's Vito balance from inside Discord — read it, charge it, or credit it back. Every charge is confirmed by the user with their PIN on vetox.io before any Vito moves, and your app is notified of the result. Vito is the Vetox in-ecosystem virtual currency; use the API to build Vito-based experiences — unlocking content, features, perks, or rewards. No real-money buying or selling takes place.
It is a REST API authenticated with a secret key. All endpoints return JSON and are versioned under /v1.
https://api.vetox.io/public/vito/v1Balances & settlement
Your API key is tied to your Vetox account. Every charge settles to you: when your app deducts Vito from a user, that Vito is credited to your own balance (minus the platform fee), never burned. When your app adds Vito to a user, it is funded from your balance.
Vito always moves between balances — never to or from real money. The user is charged the full amount you request; you receive the amount after the platform fee; credits and rewards are funded from your balance via /add.
Requirements
Access to the Vito API is granted per project. Before you can call it you need:
- An active User Membership (Silver tier or higher). The API freezes automatically if the owner's membership lapses, and restores when they resubscribe.
- An approved developer application. Submit one from this page; the Vetox team reviews it manually.
- Accepted API Developer Terms — acknowledged when you submit your developer application.
- The scopes your project needs. Scopes are granted by Vetox staff based on your application.
Available scopes
balance:readdeduct:createcredit:createtransfer:createtransactions:readAuthentication
Authenticate every request with your secret API key in the Authorization header as a Bearer token. Keys are prefixed with vito_live_.
Authorization: Bearer vito_live_xxxxxxxxxxxxxxxxTwo optional layers further protect your project:
- IP allowlist — restrict requests to specific server IPs (configure it in the Settings tab).
- Rate limits — per-project request ceilings that scale with your membership tier.
API keys — storage, rotation & security
Your key is generated when your application is approved. It is shown only once — reveal it from the API Keys tab and store it immediately.
Vetox never stores your key in readable form (only a salted hash). Keep it as a server-side secret — an environment variable or a secrets manager — never in client code, a git repository, or a browser.
Rotate from the API Keys tab. The previous key keeps working for a 24-hour grace period so you can roll out the new key with zero downtime, then it stops.
Charge confirmation flow
Every charge goes through a confirmation the user approves with their PIN — your key alone never moves Vito:
- 1Your app calls POST /deduct with the user, amount, originating guildId, and item details.
- 2Vito creates a pending confirmation (valid for 10 minutes) and returns a confirmUrl. When a guildId is given, the user is also DM'd the details with a button to the confirm page.
- 3The user opens the confirmUrl on vetox.io and approves the charge with their wallet PIN.
- 4Vito deducts the Vito, records the transaction, and — if you have a webhook configured — sends a signed confirmation.completed event to your callback URL.
- 5Your app verifies the signature and completes its action (for example, unlocks the content).
Request parameters (/deduct)
/v1/deductThe deduction endpoint accepts the following body fields:
discordIdamountguildIdreasonmerchantRefproductproduct.imageUrlmetadata* required field.
POST https://api.vetox.io/public/vito/v1/deduct
Authorization: Bearer vito_live_xxxxxxxxxxxxxxxx
Content-Type: application/json
Idempotency-Key: req_9a1f2b # optional, dedupes retries for 24h
{
"discordId": "123456789012345678",
"amount": 500,
"guildId": "1470389021162209280",
"reason": "Unlock premium theme",
"merchantRef": "ref_1042",
"product": {
"type": "feature",
"name": "Premium theme",
"description": "Unlocks the premium theme pack",
"imageUrl": "https://cdn.example.com/theme.png"
},
"metadata": { "ref": "1042" }
}{
"success": true,
"data": {
"confirmationId": "f3c9...e1",
"confirmUrl": "https://vetox.io/confirm/vito/f3c9...e1",
"amount": 500,
"expiresAt": 1735689600000,
"status": "pending"
},
"requestId": "req_abc123",
"timestamp": 1735689300000
}Adding Vito — credits (/add)
/v1/addPOST /add credits a user from your own balance — for rewards or credits. It takes the same fields as /deduct except guildId and product. Unlike a charge it is instant and needs no user PIN — your API key is your authority — so the response already reflects the completed transfer. The user receives the full amount; no platform fee applies.
POST https://api.vetox.io/public/vito/v1/add
Authorization: Bearer vito_live_xxxxxxxxxxxxxxxx
Content-Type: application/json
Idempotency-Key: credit_7f3a # optional, dedupes retries for 24h
{
"discordId": "123456789012345678",
"amount": 250,
"reason": "Event reward",
"merchantRef": "reward_1042",
"metadata": { "ref": "1042" }
}{
"success": true,
"data": {
"transactionId": "txn_91b2",
"toDiscordId": "123456789012345678",
"amount": 250,
"ownerBalance": 48250,
"recipientBalance": 1250,
"status": "completed"
},
"requestId": "req_def456",
"timestamp": 1735689300000
}Webhooks & signature verification
Add one or more https callback URLs in the Settings tab. Vito delivers a signed POST to every configured URL whenever a confirmation reaches a terminal state, retrying up to 5 times with exponential backoff.
Verifying the signature
Each delivery carries an X-Vito-Signature header of the form ts=<unix>;h1=<hex>. Recompute the HMAC over "<ts>:<rawBody>" with your signing secret and compare in constant time. Always verify against the raw request body, before JSON parsing.
signature = HMAC_SHA256(signingSecret, "<ts>:<rawBody>")
header = X-Vito-Signature: ts=<unix-seconds>;h1=<hex-signature>import crypto from 'crypto';
import express from 'express';
const app = express();
// IMPORTANT: verify against the RAW body, before JSON parsing.
app.post('/webhooks/vito', express.raw({ type: '*/*' }), (req, res) => {
const sig = req.header('X-Vito-Signature') || ''; // "ts=<unix>;h1=<hmac>[;h1=<hmac>]"
const pairs = sig.split(';').map(p => p.split('='));
const ts = pairs.find(([k]) => k === 'ts')?.[1];
const sigs = pairs.filter(([k]) => k === 'h1').map(([, v]) => v);
const rawBody = req.body.toString('utf8');
const expected = crypto
.createHmac('sha256', process.env.VITO_SIGNING_SECRET) // whsec_...
.update(`${ts}:${rawBody}`)
.digest('hex');
const expectedBuf = Buffer.from(expected, 'hex');
// During a 24h secret rotation the header carries TWO h1= values — accept ANY.
// Length-guard FIRST: crypto.timingSafeEqual throws on a length mismatch.
const ok = sigs.some(h => {
const got = Buffer.from(h, 'hex');
return (
got.length === expectedBuf.length &&
crypto.timingSafeEqual(got, expectedBuf)
);
});
// Reject anything older than ~5 minutes to stop replays.
const fresh = ts && Math.abs(Date.now() / 1000 - Number(ts)) < 300;
if (!ok || !fresh) return res.status(401).end();
const event = JSON.parse(rawBody);
if (event.eventType === 'confirmation.completed') {
grantPremiumTheme(event.data.merchantRef, event.data.fromDiscordId);
}
res.status(200).end(); // ack fast; run your action async if slow
});Webhook events & payloads
Vito sends one of four event types. They all share the same payload shape; data.status reflects the outcome.
confirmation.completedconfirmation.failedconfirmation.expiredconfirmation.cancelledcredit.completedPOST https://your-app.com/webhooks/vito
X-Vito-Signature: ts=1735689600;h1=4f8b2c...e9
X-Vito-Event-Id: evt_3a2b1c
X-Vito-Event-Type: confirmation.completed
Content-Type: application/json
{
"eventId": "evt_3a2b1c",
"eventType": "confirmation.completed",
"timestamp": 1735689600000,
"data": {
"confirmationId": "f3c9...e1",
"type": "deduct",
"fromDiscordId": "123456789012345678",
"toDiscordId": null,
"amount": 500,
"reason": "Unlock premium theme",
"merchantRef": "ref_1042",
"product": { "type": "feature", "name": "Premium theme" },
"status": "completed",
"transactionId": "txn_77a2",
"metadata": { "ref": "1042" },
"failureReason": null
}
}Error codes
Errors return a non-2xx status with a JSON body carrying a top-level error.code. Common cases:
VITO_VALIDATION_ERRORinvalid keyOWNER_MEMBERSHIP_INACTIVEscope / IPnot foundinsufficient balanceINSUFFICIENT_OWNER_FUNDSrate limited{
"success": false,
"error": { "type": "Bad Request", "message": "guildId must be a valid Discord server ID", "code": "VITO_VALIDATION_ERROR" },
"requestId": "req_abc123",
"timestamp": 1735689300000
}Integration examples
Create a charge request:
/v1/deductcurl -X POST https://api.vetox.io/public/vito/v1/deduct \
-H "Authorization: Bearer $VITO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"discordId": "123456789012345678",
"amount": 500,
"guildId": "1470389021162209280",
"merchantRef": "ref_1042",
"product": { "type": "feature", "name": "Premium theme" }
}'Check a user's balance:
/v1/balance/:discordIdcurl https://api.vetox.io/public/vito/v1/balance/123456789012345678 \
-H "Authorization: Bearer $VITO_API_KEY"Testing with the demo integration
A standalone demo integration is provided to exercise the full flow end-to-end without writing any code. It walks through request → confirmation → PIN → webhook → completion.
- 1Open the demo integration folder and add your Vito API key, a Discord bot token, and your test server (guild) ID to config.json.
- 2Run npm install && npm start, then open http://localhost:4000.
- 3Look up a user by Discord ID, pick an item, and start a charge — the demo calls /deduct and shows the confirmUrl.
- 4Approve the PIN on vetox.io; the demo detects completion (via webhook if configured, otherwise by polling) and completes the sample action.
Best practices, limits & security
Security
- Keep your API key and signing secret server-side only; rotate immediately if either leaks.
- Always verify the webhook signature against the raw body, and reject deliveries older than ~5 minutes (replay protection).
- Complete your action only on confirmation.completed — never on the /deduct response (the charge is not final at that point).
- Use the IP allowlist and request only the scopes you actually need.
Limits
- Confirmations expire after 10 minutes; treat unconfirmed requests as abandoned.
- Rate limits are per-project and scale with the owner's membership tier.
- amount must be a positive integer; metadata is capped at 10 keys.