Laat goedgekeurde apps je Vito afboeken met pincode-bevestiging, en beheer betalingsverzoeken vanaf je Aankopen-pagina.
Vereist een Silver-lidmaatschap of hoger — zowel om je aan te melden als voor elke geauthenticeerde aanroep. Verloopt het lidmaatschap van de sleutelhouder, dan wordt het project bevroren tot hij zich opnieuw abonneert.
Een REST-API waarmee je applicatie vanuit Discord met het Vito-saldo van een gebruiker werkt: uitlezen, afschrijven, bijschrijven of tussen gebruikers verplaatsen. Alle endpoints geven JSON terug en zijn geversioneerd onder /v1.
Vito wordt nooit omgezet in echt geld, en komt er ook nooit uit voort. Het beweegt uitsluitend tussen Vetox-saldi.
Elk endpoint zit onder https://api.vetox.io/public/vito.De paden op deze pagina — /v1/deduct, /v1/balance/:discordId en de rest — zijn relatief aan dat voorvoegsel, dat de SDK er voor je voor plakt. Roep je er zelf een aan, gebruik dan de volledige URL:
https://api.vetox.io/public/vito/v1/deduct
https://api.vetox.io/v1/deduct is geen route en geeft 404.
Bouw je met Node.js? Schrijf de REST-aanroepen niet zelf — gebruik het officiële pakket @vetox-bot/vito. Het dekt alle negen endpoints plus de webhookverificatie, en regelt idempotentie en nieuwe pogingen voor je. Zie Officiële Node.js-SDK hieronder.
Je sleutel en signing secret worden precies één keer getoond. Na goedkeuring heb je een venster van 7 dagen om ze te onthullen op het tabblad met API-sleutels. Vetox bewaart alleen een hash en kan ze niet opnieuw tonen — mis je het venster, dan moet je roteren.
Houd hem uitsluitend serverzijdig — wie hem heeft, kan je gebruikers afschrijven
Roteer via het tabblad met API-sleutels. De vorige sleutel blijft nog 24 uur werken als overgangsperiode, zodat je zonder downtime kunt uitrollen
Het webhook-signing-secret roteert apart, met een eigen overlap van 24 uur
Het officiële pakket @vetox-bot/vito omhult alle negen endpoints plus de webhookverificatie. Het regelt de Idempotency-Key-header, nieuwe pogingen met exponentiële backoff, time-outs en de classificatie van fouten voor je.
Vereist Node.js 20 of nieuwer. Het pakket heeft geen enkele runtime-afhankelijkheid — het gebruikt de ingebouwde fetch en node:crypto — en wordt geleverd in zowel ESM als CommonJS, met volledige TypeScript-definities.
import { VitoClient } from '@vetox-bot/vito';const vito = new VitoClient({ apiKey: process.env.VITO_API_KEY });
Laat je apiKey weg, dan leest de SDK VITO_API_KEY uit de omgeving. Het sleutelformaat wordt bij het aanmaken gecontroleerd, zodat een misvormde sleutel meteen faalt in plaats van je een netwerkronde en een 401 te kosten.
De sleutel verplaatst geld — houd hem altijd serverzijdig, nooit in een clientbundel of in de browser. console.log(vito) toont [redacted] in plaats van de sleutel, en de SDK weigert elke baseUrl met http:// naar een niet-lokale host, zodat de sleutel niet onversleuteld over de lijn gaat.
Elke methode geeft het uitgepakte data-veld rechtstreeks terug — je hoeft success of data nooit zelf uit te pakken. Alle methoden accepteren daarnaast opties per aanroep: { timeoutMs, maxRetries, signal, headers }, en de schrijfmethoden bovendien { idempotencyKey }.
const wallet = await vito.balance.retrieve('123456789012345678');if (!wallet.hasPin) { // Nog geen wallet-pincode, dus deze gebruiker kan geen afschrijving bevestigen.}
Leg de bestelling hier alleen vast als openstaand. De afschrijving heeft nog niet plaatsgevonden — lever pas bij de webhook confirmation.completed, niet bij dit antwoord.
const credit = await vito.payments.credit( { discordId: '123456789012345678', amount: 250, reason: 'Terugbetaling voor order_10423', merchantRef: 'refund_10423', }, // De sleutel afleiden uit je eigen administratie maakt het veilig om dit opnieuw // te draaien vanuit een taakwachtrij: een herhaling geeft het oorspronkelijke // resultaat terug in plaats van twee keer uit te betalen. { idempotencyKey: 'refund_10423' },);console.log(credit.transactionId, credit.ownerBalance, credit.recipientBalance);
const since = Date.now() - 24 * 60 * 60 * 1000;// Een async generator die pagina's op aanvraag ophaalt — geen handmatige bladerlus.for await (const tx of vito.transactions.iterate({ limit: 100 })) { if (tx.date < since) break; // nieuwste eerst // tx.type is de richting ten opzichte van tx.discordId: 1 = in, 0 = uit.}
De SDK stuurt bij elke schrijfactie (deduct, credit, transfer) een Idempotency-Key-header mee. Geef je er geen op, dan genereert hij de sleutel één keer per aanroep en stuurt exact dezelfde sleutel bij elke nieuwe poging opnieuw, zodat een herhaalde poging de bewerking nooit twee keer kan verrekenen.Geef je eigen sleutel mee wanneer dezelfde logische bewerking opnieuw kan worden gestart vanuit een nieuw proces — een taakrunner, een herlevering uit een wachtrij of een geplande run:
Netwerkfouten, time-outs, 408, 429, 5xx en 409 VITO_IDEMPOTENCY_IN_PROGRESS
Wordt nooit opnieuw geprobeerd
409 VITO_IDEMPOTENCY_CONFLICT, alle overige 4xx en auth.rotateKey()
auth.rotateKey() is bewust uitgesloten — een nieuwe poging maakt daar een tweede sleutel aan en maakt de sleutel uit de eerste poging ongeldig.
Is Retry-After langer dan maxRetryDelayMs (je uurquotum is dus werkelijk op), dan gooit de SDK direct VitoRateLimitError in plaats van je hele verzoekbudget te verslapen.
import { Webhooks, VitoSignatureVerificationError } from '@vetox-bot/vito';try { const event = Webhooks.constructEvent({ payload: rawRequestBody, // de ruwe body — zie de waarschuwing hieronder signature: req.header('x-vito-signature') ?? '', secret: process.env.VITO_WEBHOOK_SECRET, }); if (event.eventType === 'confirmation.completed') { // Het type van data wordt automatisch versmald op basis van eventType. await fulfil(event.data.merchantRef); }} catch (error) { if (error instanceof VitoSignatureVerificationError) { // Weiger de aflevering met een 400: vervalst, verlopen of hergeserialiseerd. }}
constructEvent controleert het replay-venster van 5 minuten, vergelijkt in constante tijd met elkeh1-handtekening in de header — het werkt dus automatisch tijdens de overlap van 24 uur bij een rotatie — en parseert daarna de payload en geeft de getypeerde gebeurtenis terug.Gebruik in een Next.js-routehandler (App Router) de variant die de ruwe body zelf leest:
export const runtime = 'nodejs'; // de verificatie gebruikt node:cryptoexport async function POST(request: Request) { const event = await Webhooks.constructEventFromRequest(request, { secret: process.env.VITO_WEBHOOK_SECRET, }); return Response.json({ received: true });}
De handtekening dekt de ruwe bytes. Koppel in Express express.raw({ type: 'application/json' }) aan de webhookroute — express.json() verbruikt de body en daarna faalt elke verificatie. Roep in Next.js request.json() niet aan vóór constructEventFromRequest.
Aflevering gebeurt minstens één keer. Dedupliceer op event.eventId voordat je een neveneffect uitvoert.
Je sleutel alleen kan het Vito van een gebruiker niet verplaatsen. Elke afschrijving vereist dat de gebruiker akkoord gaat met de pincode van zijn wallet, op vetox.io — nooit in je app en nooit in Discord.
1
Je app roept POST /v1/deduct aan
Met de gebruiker, het bedrag, de guildId van herkomst en de artikelgegevens.
2
Vito geeft een confirmUrl terug
Een openstaande bevestiging, 10 minuten geldig. De gebruiker krijgt ook een DM.
3
De gebruiker keurt goed met zijn pincode
Op vetox.io.
4
Vito verrekent en meldt
Het saldo wordt afgeschreven, de transactie vastgelegd en er wordt een ondertekende webhook verstuurd als je er een hebt ingesteld.
5
Je app verifieert en rondt af
Controleer de handtekening en ontgrendel dan de content of lever het artikel.
Rond je actie uitsluitend af bij confirmation.completed — nooit op basis van het antwoord van /deduct. Op dat moment staat de afschrijving nog niet vast.
POST /v1/add schrijft Vito bij een gebruiker bij uit je eigen saldo — voor beloningen of terugbetalingen. Dezelfde velden als /deduct, behalve guildId en product.
Anders dan een afschrijving heeft een bijschrijving geen bevestigingsstap: die wordt direct verrekend. Vereist de scope credit:create en voldoende saldo, anders geeft de aanroep 402 VITO_INSUFFICIENT_OWNER_FUNDS terug.
Elke afschrijving wordt aan jou uitbetaald minus de platformkosten — hetzelfde schema als Vito-overboekingen in de app, op basis van jouw lidmaatschapsniveau:
Jouw lidmaatschap
Kosten
Normal, Silver, Gold
7%
Platinum
6%
Diamond
5%
Bedragen van 5 Vito of minder zijn kosteloos, en bijschrijvingen via /v1/add zijn dat altijd.
Voeg op het tabblad met instellingen een of meer https-callback-URL’s toe. Vito stuurt een ondertekende POST zodra een bevestiging een eindtoestand bereikt.
Webhooks worden alleen verstuurd als je project zowel een callback-URL als een signing secret heeft. Onthul het secret (whsec_…) eenmalig op het tabblad met API-sleutels.
Elke aflevering draagt een X-Vito-Signature-header:
X-Vito-Signature: ts=<unix>;h1=<hex>
Twee andere headers vergezellen elke aflevering — gebruik X-Vito-Event-Id als deduplicatiesleutel, want een nieuwe poging stuurt hetzelfde id opnieuw:
Header
Bevat
X-Vito-Event-Id
Stabiel id voor deze gebeurtenis — identiek over alle pogingen
X-Vito-Event-Type
Bijvoorbeeld confirmation.completed
Tijdens een rotatie van het signing secret bevat de header meer dan één handtekening, de nieuwste eerst:
X-Vito-Signature: ts=<unix>;h1=<nieuw>;h1=<oud>
Accepteer de aflevering als een willekeurigeh1 klopt. Een verifier die alleen de eerste leest, wijst elke webhook af totdat het nieuwe secret is uitgerold — precies wat de overlap van 24 uur moest voorkomen.
Op Node.js: Webhooks.constructEvent uit @vetox-bot/vito doet dit allemaal voor je — het replay-venster, het matchen met elkeh1 en de vergelijking in constante tijd — en geeft de getypeerde gebeurtenis terug. De code hieronder is voor een eigen implementatie of een andere taal.
Herbereken de HMAC over <ts>:<rawBody> met je signing secret en vergelijk in constante tijd.
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) ); });}
Verifieer tegen de ruwe body, vóór JSON-parsing of middleware die hem herschrijft.
Er geldt ook een limiet per IP van de helft van je minuutquotum, met een ondergrens van 30.
Onder belasting falen schrijf-endpoints gesloten — een afschrijving wordt geweigerd in plaats van dubbele besteding te riskeren. Lees-endpoints falen open. Behandel een geweigerde schrijfactie als “niet gebeurd” en probeer opnieuw.
Stuur een Idempotency-Key-header mee om herhaalde pogingen veilig te dedupliceren.