Permite que aplicaciones aprobadas carguen sobre tu Vito con confirmación por PIN, y gestiona las solicitudes de pago desde tu página de Compras.
Requiere una Membresía Silver o superior — tanto para solicitarla como para cada llamada autenticada. Si la Membresía del titular de la clave caduca, el proyecto se congela hasta que vuelva a suscribirse.
Una API REST que permite a tu aplicación trabajar con el saldo de Vito de un usuario desde dentro de Discord: leerlo, cobrarlo, abonarlo o moverlo entre usuarios. Todos los endpoints devuelven JSON y están versionados bajo /v1.
Vito nunca se convierte en dinero real ni proviene de él. Solo se mueve entre saldos de Vetox.
Todos los endpoints están bajo https://api.vetox.io/public/vito.Las rutas que aparecen en esta página — /v1/deduct, /v1/balance/:discordId y las demás — son relativas a ese prefijo, que es lo que el SDK añade por ti. Si llamas a una directamente, usa la URL completa:
https://api.vetox.io/public/vito/v1/deduct
https://api.vetox.io/v1/deduct no es una ruta y devuelve 404.
¿Desarrollas con Node.js? No escribas las llamadas REST a mano: usa el paquete oficial @vetox-bot/vito. Cubre los nueve endpoints y la verificación de webhooks, y se encarga por ti de la idempotencia y los reintentos. Consulta SDK oficial de Node.js más abajo.
Tu clave y tu secreto de firma se muestran exactamente una vez. Tras la aprobación dispones de una ventana de 7 días para revelarlos en la pestaña de claves API. Vetox solo guarda un hash y no puede volver a mostrarlos: si pierdes la ventana, tendrás que rotar.
Guárdala solo en el servidor — cualquiera que la tenga puede cobrar a tus usuarios
Rota desde la pestaña de claves API. La clave anterior sigue funcionando durante un periodo de gracia de 24 horas para que puedas desplegar sin cortes
El secreto de firma de webhooks rota por separado, con su propio solapamiento de 24 horas
El paquete oficial @vetox-bot/vito envuelve los nueve endpoints y además la verificación de webhooks. Se encarga por ti de la cabecera Idempotency-Key, de los reintentos con espera creciente, de los tiempos de espera y de clasificar los errores.
Requiere Node.js 20 o superior. El paquete no tiene ninguna dependencia en tiempo de ejecución — usa el fetch integrado y node:crypto — y se distribuye en ESM y CommonJS con definiciones completas de TypeScript.
import { VitoClient } from '@vetox-bot/vito';const vito = new VitoClient({ apiKey: process.env.VITO_API_KEY });
Si omites apiKey, el SDK lee VITO_API_KEY del entorno. El formato de la clave se valida al construir el cliente, así que una clave mal formada falla de inmediato en lugar de costarte una ida y vuelta y un 401.
La clave mueve dinero: mantenla siempre en el servidor, nunca en un bundle de cliente ni en el navegador. console.log(vito) imprime [redacted] en lugar de la clave, y el SDK rechaza cualquier baseUrl con http:// para un host no local, para que la clave no viaje en claro.
Cada método devuelve directamente el campo data ya desenvuelto: nunca tienes que desempaquetar success ni data por tu cuenta. Además, todos aceptan opciones por llamada: { timeoutMs, maxRetries, signal, headers }, y los métodos de escritura aceptan también { idempotencyKey }.
const wallet = await vito.balance.retrieve('123456789012345678');if (!wallet.hasPin) { // Todavía no tiene PIN de monedero, así que no puede confirmar ningún cobro.}
const credit = await vito.payments.credit( { discordId: '123456789012345678', amount: 250, reason: 'Reembolso del pedido order_10423', merchantRef: 'refund_10423', }, // Derivar la clave de tu propio registro hace que sea seguro relanzarlo desde // una cola de trabajos: una repetición devuelve el resultado original en lugar // de pagar dos veces. { idempotencyKey: 'refund_10423' },);console.log(credit.transactionId, credit.ownerBalance, credit.recipientBalance);
const since = Date.now() - 24 * 60 * 60 * 1000;// Un generador asíncrono que pide páginas bajo demanda: sin bucle de paginación manual.for await (const tx of vito.transactions.iterate({ limit: 100 })) { if (tx.date < since) break; // las más recientes primero // tx.type es la dirección respecto a tx.discordId: 1 = entrada, 0 = salida.}
El SDK envía una cabecera Idempotency-Key en todas las escrituras (deduct, credit, transfer). Si no le pasas ninguna, la genera una sola vez por llamada y reenvía esa misma clave en cada reintento, de modo que un reintento nunca puede liquidar la operación dos veces.Pasa tu propia clave cuando la misma operación lógica pueda reintentarse desde un proceso nuevo: un ejecutor de trabajos, una reentrega de una cola o una tarea programada:
Fallos de red, tiempos de espera, 408, 429, 5xx y 409 VITO_IDEMPOTENCY_IN_PROGRESS
Nunca se reintenta
409 VITO_IDEMPOTENCY_CONFLICT, el resto de errores 4xx y auth.rotateKey()
auth.rotateKey() queda excluido a propósito: un reintento allí emite una segunda clave e invalida la que devolvió el primer intento.
Si Retry-After es mayor que maxRetryDelayMs (es decir, tu cuota por hora está realmente agotada), el SDK lanza VitoRateLimitError de inmediato en lugar de dormir durante todo tu presupuesto de petición.
import { Webhooks, VitoSignatureVerificationError } from '@vetox-bot/vito';try { const event = Webhooks.constructEvent({ payload: rawRequestBody, // el cuerpo en bruto — mira la advertencia de abajo signature: req.header('x-vito-signature') ?? '', secret: process.env.VITO_WEBHOOK_SECRET, }); if (event.eventType === 'confirmation.completed') { // El tipo de data se estrecha automáticamente según eventType. await fulfil(event.data.merchantRef); }} catch (error) { if (error instanceof VitoSignatureVerificationError) { // Rechaza la entrega con un 400: falsificada, caducada o cuerpo reserializado. }}
constructEvent comprueba la ventana de repetición de 5 minutos, compara en tiempo constante contra todas las firmas h1 de la cabecera — por lo que funciona automáticamente durante el solapamiento de 24 horas de la rotación — y después analiza el payload y devuelve el evento tipado.En un manejador de ruta de Next.js (App Router), usa la variante que lee el cuerpo en bruto por sí misma:
export const runtime = 'nodejs'; // la verificación usa node:cryptoexport async function POST(request: Request) { const event = await Webhooks.constructEventFromRequest(request, { secret: process.env.VITO_WEBHOOK_SECRET, }); return Response.json({ received: true });}
La firma cubre los bytes en bruto. En Express, monta express.raw({ type: 'application/json' }) en la ruta del webhook: express.json() consume el cuerpo y a partir de ahí toda verificación falla. En Next.js, no llames a request.json() antes de constructEventFromRequest.
La entrega es al menos una vez. Deduplica con event.eventId antes de ejecutar cualquier efecto secundario.
Tu clave por sí sola no puede mover el Vito de un usuario. Todo cobro exige que el usuario lo apruebe con el PIN de su monedero, en vetox.io — nunca dentro de tu app ni dentro de Discord.
1
Tu app llama a POST /v1/deduct
Con el usuario, el importe, el guildId de origen y los detalles del artículo.
2
Vito devuelve una confirmUrl
Una confirmación pendiente, válida 10 minutos. El usuario también recibe un MD.
3
El usuario aprueba con su PIN
En vetox.io.
4
Vito liquida y notifica
Se descuenta el saldo, se registra la transacción y se envía un webhook firmado si tienes uno configurado.
5
Tu app verifica y completa
Comprueba la firma y luego desbloquea el contenido o entrega el artículo.
Completa tu acción solo con confirmation.completed — nunca con la respuesta de /deduct. En ese punto el cobro todavía no es definitivo.
POST /v1/add abona Vito a un usuario desde tu propio saldo, para recompensas o reembolsos. Los mismos campos que /deduct salvo guildId y product.
A diferencia de un cobro, un abono no tiene paso de confirmación: se liquida al instante. Requiere el scope credit:create y saldo suficiente, o la llamada devuelve 402 VITO_INSUFFICIENT_OWNER_FUNDS.
Cada cobro se te liquida menos la comisión de la plataforma, con el mismo esquema que las transferencias de Vito dentro de la app y según tu nivel de Membresía:
Tu Membresía
Comisión
Normal, Silver, Gold
7 %
Platinum
6 %
Diamond
5 %
Los importes de 5 Vito o menos no tienen comisión, y los abonos vía /v1/add nunca la tienen.
Añade una o varias URL de callback https en la pestaña de ajustes. Vito entrega un POST firmado cada vez que una confirmación alcanza un estado final.
Los webhooks solo se envían si tu proyecto tiene a la vez una URL de callback y un secreto de firma. Revela el secreto (whsec_…) una única vez desde la pestaña de claves API.
Acepta la entrega si coincide cualquiera de los h1. Un verificador que solo lea el primero rechazará todos los webhooks hasta haber desplegado el secreto nuevo, que es justo lo que el solapamiento de 24 horas pretende evitar.
En Node.js: Webhooks.constructEvent de @vetox-bot/vito hace todo esto por ti — la ventana de repetición, la comparación con todas las firmas h1 y el cotejo en tiempo constante — y devuelve el evento tipado. El código de abajo es para una implementación manual o para otro lenguaje.
Recalcula el HMAC sobre <ts>:<rawBody> con tu secreto de firma y compara en tiempo constante.
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) ); });}
Verifica contra el cuerpo en bruto, antes de cualquier parseo de JSON o middleware que lo reescriba.
También existe un límite por IP igual a la mitad de tu cuota por minuto, con un mínimo de 30.
Bajo carga, los endpoints de escritura fallan en cerrado: se rechaza el cobro en lugar de arriesgar un doble gasto. Los de lectura fallan en abierto. Trata una escritura rechazada como «no ocurrió» y reinténtala.
Envía una cabecera Idempotency-Key para deduplicar los reintentos con seguridad.