> ## 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.

# API de Vito y compras

> 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.

<Info>
  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.
</Info>

Una API REST que permite a tu aplicación trabajar con el saldo de [Vito](/es/members/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`.

<Warning>
  **Vito nunca se convierte en dinero real ni proviene de él.** Solo se mueve entre saldos de Vetox.
</Warning>

## Obtener acceso

El acceso se concede **por proyecto**. Necesitas los cuatro requisitos:

<Steps>
  <Step title="Una Membresía activa, Silver o superior">
    Se comprueba en cada llamada, no solo al aprobar la solicitud.
  </Step>

  <Step title="Una solicitud de desarrollador aprobada">
    Se envía desde la página de Vito API en tu panel. La revisa manualmente el equipo de Vetox.
  </Step>

  <Step title="Los Términos para Desarrolladores de la API aceptados">
    Se aceptan al enviar la solicitud.
  </Step>

  <Step title="Los scopes que tu proyecto necesita">
    Los concede el equipo de Vetox según lo que hayas descrito.
  </Step>
</Steps>

<Tip>
  Sé concreto sobre qué estás construyendo y cómo almacenarás la clave. Las solicitudes vagas son las que se rechazan.
</Tip>

### Scopes

| Scope               | Permite                                                  |
| ------------------- | -------------------------------------------------------- |
| `balance:read`      | Leer el saldo de Vito de un usuario                      |
| `deduct:create`     | Cobrar del saldo de un usuario                           |
| `credit:create`     | Añadir Vito a un usuario, financiado con tu propio saldo |
| `transfer:create`   | Mover Vito entre dos usuarios                            |
| `transactions:read` | Listar y leer las transacciones de tu proyecto           |

## Autenticación

Envía tu clave secreta como token Bearer:

```bash theme={null}
Authorization: Bearer vito_live_xxxxxxxxxxxxxxxx
```

Dos capas opcionales refuerzan aún más un proyecto:

* **Lista de IP permitidas** — restringe las llamadas a IP de servidor concretas
* **Límites de tasa** — topes por proyecto que escalan con el nivel de Membresía del titular

### Claves, rotación y almacenamiento

<Warning>
  **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.
</Warning>

* 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
* Si se filtra, rota de inmediato

## Cobrar a un usuario

<Warning>
  **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.
</Warning>

<Steps>
  <Step title="Tu app llama a POST /v1/deduct">
    Con el usuario, el importe, el `guildId` de origen y los detalles del artículo.
  </Step>

  <Step title="Vito devuelve una confirmUrl">
    Una confirmación pendiente, válida **10 minutos**. El usuario también recibe un MD.
  </Step>

  <Step title="El usuario aprueba con su PIN">
    En `vetox.io`.
  </Step>

  <Step title="Vito liquida y notifica">
    Se descuenta el saldo, se registra la transacción y se envía un webhook firmado si tienes uno configurado.
  </Step>

  <Step title="Tu app verifica y completa">
    Comprueba la firma y luego desbloquea el contenido o entrega el artículo.
  </Step>
</Steps>

<Warning>
  **Completa tu acción solo con `confirmation.completed`** — nunca con la respuesta de `/deduct`. En ese punto el cobro todavía no es definitivo.
</Warning>

### Parámetros de la petición — `/v1/deduct`

| Campo         | Obligatorio | Notas                                                                                       |
| ------------- | ----------- | ------------------------------------------------------------------------------------------- |
| `discordId`   | **Sí**      | El ID de Discord del usuario — un snowflake de 17 a 20 dígitos                              |
| `amount`      | **Sí**      | Entero positivo                                                                             |
| `guildId`     | **Sí**      | El servidor de Discord del que parte el cobro. Todo cobro debe originarse en un servidor    |
| `reason`      | No          | Hasta 256 caracteres. Se muestra al usuario y se devuelve en el webhook                     |
| `merchantRef` | No          | Tu propia referencia, hasta 128 caracteres. Úsala para casar el webhook con tus registros   |
| `product`     | No          | `{ type, name, description?, imageUrl? }` — aparece en la página de confirmación y en el MD |
| `imageUrl`    | No          | Debe ser `https://`                                                                         |
| `metadata`    | No          | Hasta **10** pares clave/valor de texto, que se transmiten tal cual                         |

## Abonar Vito a un usuario

`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`.

<Note>
  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`**.
</Note>

## Comisiones

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 %      |

<Note>
  Los importes de **5 Vito o menos no tienen comisión**, y los abonos vía `/v1/add` nunca la tienen.
</Note>

## Webhooks

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.

<Warning>
  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.
</Warning>

### Verificar la firma

Cada entrega lleva una cabecera `X-Vito-Signature`:

```text theme={null}
X-Vito-Signature: ts=<unix>;h1=<hex>
```

Otras dos cabeceras acompañan cada entrega — usa `X-Vito-Event-Id` como clave de deduplicación, ya que un reintento reenvía el mismo id:

| Cabecera            | Contiene                                                     |
| ------------------- | ------------------------------------------------------------ |
| `X-Vito-Event-Id`   | Id estable de este evento — idéntico en todos los reintentos |
| `X-Vito-Event-Type` | Por ejemplo `confirmation.completed`                         |

<Warning>
  **Durante una rotación del secreto de firma la cabecera lleva más de una firma**, la más nueva primero:

  ```text theme={null}
  X-Vito-Signature: ts=<unix>;h1=<nueva>;h1=<antigua>
  ```

  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.
</Warning>

Recalcula el HMAC sobre `<ts>:<rawBody>` con tu secreto de firma y compara en tiempo constante.

```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>
  Verifica contra el **cuerpo en bruto**, antes de cualquier parseo de JSON o middleware que lo reescriba.
</Warning>

### Eventos

Cinco tipos de evento, todos con la misma forma de payload. `data.status` lleva el resultado.

| Evento                   | Significado                                                                                      |
| ------------------------ | ------------------------------------------------------------------------------------------------ |
| `confirmation.completed` | Aprobado y cobrado. Incluye `transactionId`. **Completa tu acción aquí**                         |
| `confirmation.failed`    | No se pudo completar — saldo insuficiente o error interno. Incluye `failureReason`               |
| `confirmation.expired`   | No se confirmó en 10 minutos. No se movió ningún Vito                                            |
| `confirmation.cancelled` | El usuario lo canceló. No se movió ningún Vito                                                   |
| `credit.completed`       | Se liquidó un `/add` financiado por el titular. Se dispara al instante, sin paso de confirmación |

<Note>
  Los webhooks se reintentan **5 veces con espera creciente**. Responde 2xx rápido y haz tu entrega de forma asíncrona.
</Note>

## Códigos de error

Toda respuesta va envuelta. Un éxito lleva `data` y un fallo lleva `error`, nunca ambos:

```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>
  **Todos los códigos llevan el prefijo `VITO_`.** Compara la cadena completa: un `RATE_LIMITED` o `FORBIDDEN` a secas nunca aparece en la respuesta.
</Warning>

| Código                                | Estado | Significado                                                                          |
| ------------------------------------- | ------ | ------------------------------------------------------------------------------------ |
| `VITO_VALIDATION_ERROR`               | 400    | Falta un campo o tiene un formato incorrecto                                         |
| `VITO_INVALID_AMOUNT`                 | 400    | `amount` no es un entero positivo                                                    |
| `VITO_AMOUNT_LIMIT_EXCEEDED`          | 400    | Supera tu tope por transacción                                                       |
| `VITO_DAILY_VOLUME_EXCEEDED`          | 400    | Esta llamada rebasaría tu tope de volumen diario                                     |
| `VITO_SELF_TRANSFER`                  | 400    | Emisor y receptor son el mismo usuario                                               |
| `VITO_INVALID_API_KEY`                | 401    | Clave ausente, mal formada, revocada o desconocida                                   |
| `VITO_INSUFFICIENT_FUNDS`             | 402    | El usuario no puede cubrir el cobro                                                  |
| `VITO_INSUFFICIENT_OWNER_FUNDS`       | 402    | **Tu** saldo es insuficiente para financiar un `/add`                                |
| `VITO_INSUFFICIENT_SCOPE`             | 403    | La clave no tiene el scope que exige el endpoint                                     |
| `VITO_IP_NOT_ALLOWED`                 | 403    | La IP que llama no está en la lista de permitidas                                    |
| `VITO_OWNER_MEMBERSHIP_INACTIVE`      | 403    | La Membresía del titular caducó — congelado hasta que se resuscriba                  |
| `VITO_GUILD_NOT_ALLOWED`              | 403    | `guildId` no está en la lista de servidores permitidos del proyecto                  |
| `VITO_TOS_NOT_ACCEPTED`               | 403    | Términos para desarrolladores no aceptados, o hay una versión más reciente pendiente |
| `VITO_PROJECT_FROZEN`                 | 403    | Congelado — normalmente por una Membresía del titular caducada                       |
| `VITO_PROJECT_SUSPENDED`              | 403    | Suspendido por el equipo de Vetox                                                    |
| `VITO_PROJECT_BANNED`                 | 403    | Baneado por el equipo de Vetox                                                       |
| `VITO_USER_BLACKLISTED`               | 403    | El usuario está excluido de las operaciones con Vito                                 |
| `VITO_ACCOUNT_LOCKED`                 | 403    | El monedero del usuario está bloqueado tras intentos fallidos de PIN                 |
| `VITO_USER_NOT_FOUND`                 | 404    | No hay cuenta de Vito para ese ID de Discord                                         |
| `VITO_CONFIRMATION_NOT_FOUND`         | 404    | Token de confirmación desconocido                                                    |
| `VITO_IDEMPOTENCY_CONFLICT`           | 409    | Mismo `Idempotency-Key` con un cuerpo distinto                                       |
| `VITO_IDEMPOTENCY_IN_PROGRESS`        | 409    | Una petición idéntica sigue en curso — reinténtalo en breve                          |
| `VITO_CONFIRMATION_ALREADY_PROCESSED` | 409    | Esa confirmación ya alcanzó un estado final                                          |
| `VITO_KEY_ROTATION_IN_PROGRESS`       | 409    | Ya hay una rotación en marcha                                                        |
| `VITO_CONFIRMATION_EXPIRED`           | 410    | Transcurrió la ventana de 10 minutos                                                 |
| `VITO_RATE_LIMITED`                   | 429    | Reduce el ritmo y reinténtalo                                                        |
| `VITO_INTERNAL_ERROR`                 | 500    | Fallo inesperado por nuestra parte                                                   |

## Límites de tasa

| Membresía del titular | Por minuto | Por hora |
| --------------------- | ---------- | -------- |
| Ninguna               | 60         | 1.000    |
| Silver o Gold         | 180        | 5.000    |
| Platinum o Diamond    | 600        | 15.000   |

También existe un límite por IP igual a la mitad de tu cuota por minuto, con un mínimo de 30.

<Warning>
  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.
</Warning>

<Note>
  Envía una cabecera `Idempotency-Key` para deduplicar los reintentos con seguridad.
</Note>

## Endpoints

| Endpoint                        | Scope               |
| ------------------------------- | ------------------- |
| `GET /v1/auth/verify`           | cualquiera          |
| `POST /v1/auth/rotate-key`      | cualquiera          |
| `GET /v1/balance/:discordId`    | `balance:read`      |
| `POST /v1/deduct`               | `deduct:create`     |
| `POST /v1/add`                  | `credit:create`     |
| `POST /v1/transfer`             | `transfer:create`   |
| `GET /v1/transactions` y `/:id` | `transactions:read` |
| `GET /v1/webhooks/events`       | `transactions:read` |

## Límites

* Las confirmaciones caducan a los **10 minutos** — trata las no confirmadas como abandonadas
* `amount` debe ser un entero positivo
* `metadata` está limitado a 10 claves
* Los topes por transacción y diarios los fija el equipo de Vetox y aparecen en modo lectura en la pestaña de ajustes

## Lista de comprobación de seguridad

<AccordionGroup>
  <Accordion title="Mantén los secretos en el servidor" icon="lock">
    La clave API y el secreto de firma jamás deben estar en código de cliente. Rota de inmediato si alguno se filtra.
  </Accordion>

  <Accordion title="Verifica cada webhook" icon="signature">
    Comprueba la firma contra el cuerpo en bruto y rechaza las entregas de más de \~5 minutos.
  </Accordion>

  <Accordion title="Liquida solo con completed" icon="circle-check">
    Nunca entregues basándote en la respuesta de `/deduct`: el cobro no es firme hasta `confirmation.completed`.
  </Accordion>

  <Accordion title="Mínimo privilegio" icon="key">
    Pide solo los scopes que realmente usas y activa la lista de IP permitidas.
  </Accordion>
</AccordionGroup>

## Resolución de problemas

<AccordionGroup>
  <Accordion title="Todas las llamadas devuelven no autorizado">
    La Membresía del titular ha caducado. Se comprueba en cada llamada.
  </Accordion>

  <Accordion title="He perdido mi clave">
    No se puede recuperar: solo se almacena un hash. Rota para obtener una nueva.
  </Accordion>

  <Accordion title="Las firmas de webhook fallan tras rotar">
    Acepta ambos secretos durante el solapamiento de 24 horas.
  </Accordion>

  <Accordion title="Un cobro nunca se completa">
    El usuario no lo aprobó. Las confirmaciones caducan a los 10 minutos.
  </Accordion>

  <Accordion title="No llega ningún webhook">
    Un proyecto necesita URL de callback y secreto de firma. Con solo uno de los dos no se entrega nada.
  </Accordion>

  <Accordion title="Llegó menos Vito del que cobré">
    Es la comisión de liquidación. Usa importes de 5 Vito o menos para evitarla, o inclúyela en tu precio.
  </Accordion>

  <Accordion title="402 VITO_INSUFFICIENT_OWNER_FUNDS">
    `/v1/add` se financia con tu propio saldo, no se crea de la nada. Recarga.
  </Accordion>
</AccordionGroup>

<CardGroup cols={2}>
  <Card title="Vito" icon="coins" href="/es/members/vito">
    Saldos, el PIN y las comisiones.
  </Card>

  <Card title="Solicitudes de pago" icon="receipt" href="/es/account/payment-requests">
    Lo que ve el usuario cuando le cobras.
  </Card>
</CardGroup>
