> ## 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 Vito e acquisti

> Consenti alle app approvate di addebitare i tuoi Vito con conferma via PIN e gestisci le richieste di pagamento dalla tua pagina Acquisti.

<Info>
  Richiede un abbonamento **Silver** o superiore — sia per candidarsi sia per ogni chiamata autenticata. Se l'abbonamento del titolare della chiave scade, il progetto viene congelato finché non si riabbona.
</Info>

Un'API REST che permette alla tua applicazione di operare sul saldo [Vito](/it/members/vito) di un utente da dentro Discord: leggerlo, addebitarlo, accreditarlo o spostarlo tra utenti. Tutti gli endpoint restituiscono JSON e sono versionati sotto `/v1`.

<Warning>
  **I Vito non si convertono mai in denaro reale, né viceversa.** Si spostano soltanto tra saldi Vetox.
</Warning>

## Ottenere l'accesso

L'accesso viene concesso **per progetto**. Servono tutti e quattro i requisiti:

<Steps>
  <Step title="Un abbonamento attivo, Silver o superiore">
    Verificato a ogni chiamata, non solo in fase di approvazione.
  </Step>

  <Step title="Una richiesta da sviluppatore approvata">
    Si invia dalla pagina Vito API della dashboard. Viene esaminata manualmente dal team Vetox.
  </Step>

  <Step title="I Termini per sviluppatori dell'API accettati">
    Si accettano al momento dell'invio della richiesta.
  </Step>

  <Step title="Gli scope di cui il progetto ha bisogno">
    Concessi dal team Vetox in base a quanto hai descritto.
  </Step>
</Steps>

<Tip>
  Sii concreto su cosa stai costruendo e su come conserverai la chiave. Sono le richieste vaghe a essere respinte.
</Tip>

### Scope

| Scope               | Consente                                               |
| ------------------- | ------------------------------------------------------ |
| `balance:read`      | Leggere il saldo Vito di un utente                     |
| `deduct:create`     | Addebitare il saldo di un utente                       |
| `credit:create`     | Accreditare Vito a un utente, finanziati dal tuo saldo |
| `transfer:create`   | Spostare Vito tra due utenti                           |
| `transactions:read` | Elencare e leggere le transazioni del tuo progetto     |

## Autenticazione

Invia la tua chiave segreta come token Bearer:

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

Due livelli facoltativi rafforzano ulteriormente un progetto:

* **Allowlist di IP** — limita le chiamate a IP di server specifici
* **Limiti di frequenza** — tetti per progetto che crescono con il livello di abbonamento del titolare

### Chiavi, rotazione e conservazione

<Warning>
  **La chiave e il segreto di firma vengono mostrati esattamente una volta.** Dopo l'approvazione hai una **finestra di 7 giorni** per rivelarli dalla scheda delle chiavi API. Vetox conserva solo un hash e non può mostrarli di nuovo: se perdi la finestra devi effettuare una rotazione.
</Warning>

* Tienila **solo lato server** — chiunque la possieda può addebitare i tuoi utenti
* Ruota dalla scheda delle chiavi API. La chiave precedente resta valida per un **periodo di tolleranza di 24 ore**, così puoi rilasciare senza interruzioni
* Il segreto di firma dei webhook si ruota separatamente, con una sovrapposizione di 24 ore propria
* In caso di fuga, ruota subito

## Addebitare un utente

<Warning>
  **La tua chiave da sola non può spostare i Vito di un utente.** Ogni addebito richiede l'approvazione dell'utente con il PIN del suo portafoglio, su `vetox.io` — mai dentro la tua app né dentro Discord.
</Warning>

<Steps>
  <Step title="La tua app chiama POST /v1/deduct">
    Con l'utente, l'importo, il `guildId` di origine e i dettagli dell'articolo.
  </Step>

  <Step title="Vito restituisce una confirmUrl">
    Una conferma in sospeso, valida **10 minuti**. L'utente riceve anche un MP.
  </Step>

  <Step title="L'utente approva con il suo PIN">
    Su `vetox.io`.
  </Step>

  <Step title="Vito liquida e notifica">
    Il saldo viene addebitato, la transazione registrata e viene inviato un webhook firmato, se ne hai configurato uno.
  </Step>

  <Step title="La tua app verifica e completa">
    Controlla la firma, poi sblocca il contenuto o consegna l'articolo.
  </Step>
</Steps>

<Warning>
  **Completa la tua azione solo su `confirmation.completed`** — mai sulla risposta di `/deduct`. A quel punto l'addebito non è ancora definitivo.
</Warning>

### Parametri della richiesta — `/v1/deduct`

| Campo         | Obbligatorio | Note                                                                                   |
| ------------- | ------------ | -------------------------------------------------------------------------------------- |
| `discordId`   | **Sì**       | L'ID Discord dell'utente — uno snowflake da 17 a 20 cifre                              |
| `amount`      | **Sì**       | Numero intero positivo                                                                 |
| `guildId`     | **Sì**       | Il server Discord da cui parte l'addebito. Ogni addebito deve provenire da un server   |
| `reason`      | No           | Fino a 256 caratteri. Mostrato all'utente e restituito nel webhook                     |
| `merchantRef` | No           | Un tuo riferimento, fino a 128 caratteri. Serve a collegare il webhook ai tuoi archivi |
| `product`     | No           | `{ type, name, description?, imageUrl? }` — compare nella pagina di conferma e nel MP  |
| `imageUrl`    | No           | Deve essere `https://`                                                                 |
| `metadata`    | No           | Fino a **10** coppie chiave/valore testuali, trasmesse così come sono                  |

## Accreditare un utente

`POST /v1/add` accredita Vito a un utente **dal tuo saldo** — per premi o rimborsi. Stessi campi di `/deduct` tranne `guildId` e `product`.

<Note>
  A differenza di un addebito, un accredito **non ha una fase di conferma**: viene liquidato subito. Richiede lo scope `credit:create` e saldo sufficiente, altrimenti la chiamata restituisce **402 `VITO_INSUFFICIENT_OWNER_FUNDS`**.
</Note>

## Commissioni

Ogni addebito ti viene liquidato al netto della commissione della piattaforma, secondo lo stesso schema dei trasferimenti Vito nell'app e in base al **tuo** livello di abbonamento:

| Il tuo abbonamento   | Commissione |
| -------------------- | ----------- |
| Normal, Silver, Gold | 7 %         |
| Platinum             | 6 %         |
| Diamond              | 5 %         |

<Note>
  Gli importi di **5 Vito o meno sono esenti da commissioni**, e gli accrediti tramite `/v1/add` lo sono sempre.
</Note>

## Webhook

Aggiungi uno o più URL di callback `https` nella scheda delle impostazioni. Vito invia un `POST` firmato ogni volta che una conferma raggiunge uno stato finale.

<Warning>
  I webhook partono solo se il progetto ha **sia** un URL di callback **sia** un segreto di firma. Rivela il segreto (`whsec_…`) una sola volta dalla scheda delle chiavi API.
</Warning>

### Verificare la firma

Ogni consegna porta un header `X-Vito-Signature`:

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

Altri due header accompagnano ogni consegna — usa `X-Vito-Event-Id` come chiave di deduplicazione, perché un nuovo tentativo rimanda lo stesso id:

| Header              | Contiene                                                    |
| ------------------- | ----------------------------------------------------------- |
| `X-Vito-Event-Id`   | Id stabile di questo evento — identico tra i vari tentativi |
| `X-Vito-Event-Type` | Ad esempio `confirmation.completed`                         |

<Warning>
  **Durante una rotazione del segreto di firma l'header porta più di una firma**, la più recente per prima:

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

  Accetta la consegna se corrisponde **una qualsiasi** delle `h1`. Un verificatore che legge solo la prima rifiuterà ogni webhook finché non avrà rilasciato il nuovo segreto, vanificando lo scopo della sovrapposizione di 24 ore.
</Warning>

Ricalcola l'HMAC su `<ts>:<rawBody>` con il tuo segreto di firma e confronta a tempo costante.

```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 sul **corpo grezzo**, prima di qualunque parsing JSON o middleware che lo riscriva.
</Warning>

### Eventi

Cinque tipi di evento, tutti con la stessa struttura di payload. `data.status` porta l'esito.

| Evento                   | Significato                                                                                |
| ------------------------ | ------------------------------------------------------------------------------------------ |
| `confirmation.completed` | Approvato e addebitato. Contiene `transactionId`. **Completa qui la tua azione**           |
| `confirmation.failed`    | Impossibile completare — saldo insufficiente o errore interno. Contiene `failureReason`    |
| `confirmation.expired`   | Non confermato entro 10 minuti. Nessun Vito spostato                                       |
| `confirmation.cancelled` | Annullato dall'utente. Nessun Vito spostato                                                |
| `credit.completed`       | Un `/add` finanziato dal titolare è stato liquidato. Parte subito — senza fase di conferma |

<Note>
  I webhook vengono ritentati **5 volte con backoff**. Rispondi rapidamente con 2xx ed esegui la consegna in modo asincrono.
</Note>

## Codici di errore

Ogni risposta è incapsulata. Un successo porta `data`, un errore porta `error`, mai entrambi:

```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>
  **Ogni codice ha il prefisso `VITO_`.** Confronta la stringa completa: un `RATE_LIMITED` o `FORBIDDEN` da solo non compare mai nelle risposte.
</Warning>

| Codice                                | Stato | Significato                                                                    |
| ------------------------------------- | ----- | ------------------------------------------------------------------------------ |
| `VITO_VALIDATION_ERROR`               | 400   | Un campo manca o è malformato                                                  |
| `VITO_INVALID_AMOUNT`                 | 400   | `amount` non è un intero positivo                                              |
| `VITO_AMOUNT_LIMIT_EXCEEDED`          | 400   | Oltre il tuo tetto per transazione                                             |
| `VITO_DAILY_VOLUME_EXCEEDED`          | 400   | Questa chiamata supererebbe il tuo tetto di volume giornaliero                 |
| `VITO_SELF_TRANSFER`                  | 400   | Mittente e destinatario sono lo stesso utente                                  |
| `VITO_INVALID_API_KEY`                | 401   | Chiave assente, malformata, revocata o sconosciuta                             |
| `VITO_INSUFFICIENT_FUNDS`             | 402   | L'utente non può coprire l'addebito                                            |
| `VITO_INSUFFICIENT_OWNER_FUNDS`       | 402   | Il **tuo** saldo è insufficiente per finanziare un `/add`                      |
| `VITO_INSUFFICIENT_SCOPE`             | 403   | Alla chiave manca lo scope richiesto dall'endpoint                             |
| `VITO_IP_NOT_ALLOWED`                 | 403   | L'IP chiamante non è nell'allowlist                                            |
| `VITO_OWNER_MEMBERSHIP_INACTIVE`      | 403   | L'abbonamento del titolare è scaduto — congelato fino al rinnovo               |
| `VITO_GUILD_NOT_ALLOWED`              | 403   | `guildId` non è nell'elenco dei server consentiti del progetto                 |
| `VITO_TOS_NOT_ACCEPTED`               | 403   | Termini per sviluppatori non accettati, o è in attesa una versione più recente |
| `VITO_PROJECT_FROZEN`                 | 403   | Congelato — di solito per l'abbonamento scaduto del titolare                   |
| `VITO_PROJECT_SUSPENDED`              | 403   | Sospeso dal team Vetox                                                         |
| `VITO_PROJECT_BANNED`                 | 403   | Bandito dal team Vetox                                                         |
| `VITO_USER_BLACKLISTED`               | 403   | L'utente è escluso dalle operazioni Vito                                       |
| `VITO_ACCOUNT_LOCKED`                 | 403   | Il portafoglio dell'utente è bloccato dopo tentativi di PIN falliti            |
| `VITO_USER_NOT_FOUND`                 | 404   | Nessun account Vito per quell'ID Discord                                       |
| `VITO_CONFIRMATION_NOT_FOUND`         | 404   | Token di conferma sconosciuto                                                  |
| `VITO_IDEMPOTENCY_CONFLICT`           | 409   | Stesso `Idempotency-Key`, corpo diverso                                        |
| `VITO_IDEMPOTENCY_IN_PROGRESS`        | 409   | Una richiesta identica è ancora in corso — riprova a breve                     |
| `VITO_CONFIRMATION_ALREADY_PROCESSED` | 409   | Quella conferma ha già raggiunto uno stato finale                              |
| `VITO_KEY_ROTATION_IN_PROGRESS`       | 409   | Una rotazione è già in corso                                                   |
| `VITO_CONFIRMATION_EXPIRED`           | 410   | La finestra di 10 minuti è trascorsa                                           |
| `VITO_RATE_LIMITED`                   | 429   | Rallenta e riprova                                                             |
| `VITO_INTERNAL_ERROR`                 | 500   | Guasto inatteso dalla nostra parte                                             |

## Limiti di frequenza

| Abbonamento del titolare | Al minuto | All'ora |
| ------------------------ | --------- | ------- |
| Nessuno                  | 60        | 1.000   |
| Silver o Gold            | 180       | 5.000   |
| Platinum o Diamond       | 600       | 15.000  |

Esiste anche un limite per IP pari alla metà della tua quota al minuto, con un minimo di 30.

<Warning>
  Sotto carico **gli endpoint di scrittura falliscono in modo chiuso** — un addebito viene rifiutato anziché rischiare una doppia spesa. Le letture falliscono in modo aperto. Tratta una scrittura rifiutata come «non avvenuta» e riprova.
</Warning>

<Note>
  Invia un header `Idempotency-Key` per deduplicare i tentativi in sicurezza.
</Note>

## Endpoint

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

## Limiti

* Le conferme scadono dopo **10 minuti** — considera abbandonate le richieste non confermate
* `amount` deve essere un intero positivo
* `metadata` è limitato a 10 chiavi
* I tetti per transazione e giornalieri sono fissati dal team Vetox e appaiono in sola lettura nella scheda delle impostazioni

## Checklist di sicurezza

<AccordionGroup>
  <Accordion title="Tieni i segreti lato server" icon="lock">
    La chiave API e il segreto di firma non vanno mai nel codice client. Se uno dei due trapela, ruota subito.
  </Accordion>

  <Accordion title="Verifica ogni webhook" icon="signature">
    Controlla la firma sul corpo grezzo e rifiuta le consegne più vecchie di \~5 minuti.
  </Accordion>

  <Accordion title="Liquida solo su completed" icon="circle-check">
    Non consegnare mai sulla base della risposta di `/deduct`: l'addebito è definitivo solo con `confirmation.completed`.
  </Accordion>

  <Accordion title="Privilegio minimo" icon="key">
    Richiedi solo gli scope che usi davvero e attiva l'allowlist di IP.
  </Accordion>
</AccordionGroup>

## Risoluzione dei problemi

<AccordionGroup>
  <Accordion title="Ogni chiamata restituisce non autorizzato">
    L'abbonamento del titolare è scaduto. Viene ricontrollato a ogni chiamata.
  </Accordion>

  <Accordion title="Ho perso la mia chiave">
    Non è recuperabile — viene conservato solo un hash. Ruota per ottenerne una nuova.
  </Accordion>

  <Accordion title="Le firme dei webhook falliscono dopo la rotazione">
    Accetta entrambi i segreti durante la sovrapposizione di 24 ore.
  </Accordion>

  <Accordion title="Un addebito non si completa mai">
    L'utente non l'ha approvato. Le conferme scadono dopo 10 minuti.
  </Accordion>

  <Accordion title="Non arriva nessun webhook">
    Un progetto ha bisogno di URL di callback e segreto di firma. Con uno solo dei due non viene consegnato nulla.
  </Accordion>

  <Accordion title="Sono arrivati meno Vito di quelli addebitati">
    È la commissione di liquidazione. Usa importi di 5 Vito o meno per evitarla, oppure includila nel prezzo.
  </Accordion>

  <Accordion title="402 VITO_INSUFFICIENT_OWNER_FUNDS">
    `/v1/add` è finanziato dal tuo saldo, non creato dal nulla. Ricarica.
  </Accordion>
</AccordionGroup>

<CardGroup cols={2}>
  <Card title="Vito" icon="coins" href="/it/members/vito">
    Saldi, PIN e commissioni.
  </Card>

  <Card title="Richieste di pagamento" icon="receipt" href="/it/account/payment-requests">
    Cosa vede l'utente quando lo addebiti.
  </Card>
</CardGroup>
