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

# Vito API et achats

> Autorisez les applications approuvées à prélever vos Vito avec confirmation par PIN, et gérez les demandes de paiement depuis votre page Achats.

<Info>
  Nécessite un abonnement **Silver** ou supérieur — à la fois pour postuler et pour chaque appel authentifié. Si l'abonnement du détenteur de la clé expire, le projet est gelé jusqu'à son renouvellement.
</Info>

Une API REST qui permet à votre application de manipuler le solde [Vito](/fr/members/vito) d'un utilisateur depuis Discord : le lire, le débiter, le créditer ou le déplacer entre utilisateurs. Tous les endpoints renvoient du JSON et sont versionnés sous `/v1`.

<Warning>
  **Les Vito ne se convertissent jamais en argent réel, ni l'inverse.** Ils circulent uniquement entre les soldes Vetox.
</Warning>

## Obtenir l'accès

L'accès est accordé **par projet**. Les quatre conditions sont requises :

<Steps>
  <Step title="Un abonnement actif, Silver ou supérieur">
    Vérifié à chaque appel, pas seulement à l'approbation.
  </Step>

  <Step title="Une demande de développeur approuvée">
    Soumise depuis la page Vito API de votre tableau de bord. Examinée manuellement par l'équipe Vetox.
  </Step>

  <Step title="Les conditions développeur de l'API acceptées">
    Validées au moment de soumettre la demande.
  </Step>

  <Step title="Les scopes dont votre projet a besoin">
    Accordés par l'équipe Vetox selon ce que vous avez décrit.
  </Step>
</Steps>

<Tip>
  Soyez précis sur ce que vous construisez et sur la façon dont vous stockerez la clé. Ce sont les demandes vagues qui sont refusées.
</Tip>

### Scopes

| Scope               | Autorise                                                            |
| ------------------- | ------------------------------------------------------------------- |
| `balance:read`      | Lire le solde Vito d'un utilisateur                                 |
| `deduct:create`     | Débiter le solde d'un utilisateur                                   |
| `credit:create`     | Créditer des Vito à un utilisateur, financés sur votre propre solde |
| `transfer:create`   | Déplacer des Vito entre deux utilisateurs                           |
| `transactions:read` | Lister et lire les transactions de votre projet                     |

## Authentification

Envoyez votre clé secrète comme jeton Bearer :

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

Deux couches facultatives renforcent encore un projet :

* **Liste d'IP autorisées** — restreindre les appels à des IP de serveur précises
* **Limites de débit** — plafonds par projet qui augmentent avec le niveau d'abonnement du détenteur

### Clés, rotation et stockage

<Warning>
  **Votre clé et votre secret de signature ne sont affichés qu'une seule fois.** Après l'approbation, vous disposez d'une **fenêtre de 7 jours** pour les révéler depuis l'onglet des clés API. Vetox n'en conserve qu'un hachage et ne peut pas les réafficher — si vous manquez la fenêtre, il faut effectuer une rotation.
</Warning>

* Gardez-la **uniquement côté serveur** — quiconque la détient peut débiter vos utilisateurs
* Effectuez la rotation depuis l'onglet des clés API. La clé précédente reste valable pendant un **délai de grâce de 24 heures**, ce qui permet de déployer sans interruption
* Le secret de signature des webhooks se renouvelle séparément, avec son propre chevauchement de 24 heures
* En cas de fuite, effectuez immédiatement une rotation

## Débiter un utilisateur

<Warning>
  **Votre clé seule ne peut pas déplacer les Vito d'un utilisateur.** Chaque débit exige que l'utilisateur l'approuve avec le code PIN de son portefeuille, sur `vetox.io` — jamais dans votre application ni dans Discord.
</Warning>

<Steps>
  <Step title="Votre application appelle POST /v1/deduct">
    Avec l'utilisateur, le montant, le `guildId` d'origine et les détails de l'article.
  </Step>

  <Step title="Vito renvoie une confirmUrl">
    Une confirmation en attente, valable **10 minutes**. L'utilisateur reçoit également un MP.
  </Step>

  <Step title="L'utilisateur approuve avec son code PIN">
    Sur `vetox.io`.
  </Step>

  <Step title="Vito règle et notifie">
    Le solde est débité, la transaction enregistrée, et un webhook signé est envoyé si vous en avez configuré un.
  </Step>

  <Step title="Votre application vérifie et finalise">
    Vérifiez la signature, puis débloquez le contenu ou livrez l'article.
  </Step>
</Steps>

<Warning>
  **Ne finalisez votre action que sur `confirmation.completed`** — jamais sur la réponse de `/deduct`. À ce stade, le débit n'est pas définitif.
</Warning>

### Paramètres de la requête — `/v1/deduct`

| Champ         | Requis  | Notes                                                                                               |
| ------------- | ------- | --------------------------------------------------------------------------------------------------- |
| `discordId`   | **Oui** | L'identifiant Discord de l'utilisateur — un snowflake de 17 à 20 chiffres                           |
| `amount`      | **Oui** | Entier positif                                                                                      |
| `guildId`     | **Oui** | Le serveur Discord d'où provient le débit. Tout débit doit provenir d'un serveur                    |
| `reason`      | Non     | Jusqu'à 256 caractères. Affiché à l'utilisateur et renvoyé dans le webhook                          |
| `merchantRef` | Non     | Votre propre référence, jusqu'à 128 caractères. Sert à rapprocher le webhook de vos enregistrements |
| `product`     | Non     | `{ type, name, description?, imageUrl? }` — affiché sur la page de confirmation et dans le MP       |
| `imageUrl`    | Non     | Doit être en `https://`                                                                             |
| `metadata`    | Non     | Jusqu'à **10** paires clé/valeur textuelles, transmises telles quelles                              |

## Créditer un utilisateur

`POST /v1/add` crédite des Vito à un utilisateur **depuis votre propre solde** — pour des récompenses ou des remboursements. Mêmes champs que `/deduct`, sauf `guildId` et `product`.

<Note>
  Contrairement à un débit, un crédit **n'a pas d'étape de confirmation** : il est réglé immédiatement. Nécessite le scope `credit:create` et un solde suffisant, sinon l'appel renvoie **402 `VITO_INSUFFICIENT_OWNER_FUNDS`**.
</Note>

## Frais

Chaque débit vous est reversé après déduction des frais de la plateforme — selon le même barème que les transferts Vito dans l'application, en fonction de **votre** niveau d'abonnement :

| Votre abonnement     | Frais |
| -------------------- | ----- |
| Normal, Silver, Gold | 7 %   |
| Platinum             | 6 %   |
| Diamond              | 5 %   |

<Note>
  Les montants de **5 Vito ou moins sont sans frais**, et les crédits via `/v1/add` le sont toujours.
</Note>

## Webhooks

Ajoutez une ou plusieurs URL de rappel `https` dans l'onglet des paramètres. Vito envoie un `POST` signé dès qu'une confirmation atteint un état final.

<Warning>
  Les webhooks ne partent que si votre projet possède **à la fois** une URL de rappel **et** un secret de signature. Révélez le secret (`whsec_…`) une seule fois depuis l'onglet des clés API.
</Warning>

### Vérifier la signature

Chaque livraison porte un en-tête `X-Vito-Signature` :

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

Deux autres en-têtes accompagnent chaque livraison — utilisez `X-Vito-Event-Id` comme clé de déduplication, car une nouvelle tentative renvoie le même identifiant :

| En-tête             | Contient                                                                  |
| ------------------- | ------------------------------------------------------------------------- |
| `X-Vito-Event-Id`   | Identifiant stable de cet événement — identique d'une tentative à l'autre |
| `X-Vito-Event-Type` | Par exemple `confirmation.completed`                                      |

<Warning>
  **Pendant une rotation du secret de signature, l'en-tête porte plusieurs signatures**, la plus récente en premier :

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

  Acceptez la livraison si **l'un quelconque** des `h1` correspond. Un vérificateur qui ne lit que le premier rejettera tous les webhooks tant qu'il n'aura pas déployé le nouveau secret — ce qui annule tout l'intérêt du chevauchement de 24 heures.
</Warning>

Recalculez le HMAC sur `<ts>:<rawBody>` avec votre secret de signature et comparez en temps constant.

```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>
  Vérifiez sur le **corps brut**, avant tout parsing JSON ou middleware susceptible de le réécrire.
</Warning>

### Événements

Cinq types d'événements, partageant tous la même structure de charge utile. `data.status` porte le résultat.

| Événement                | Signification                                                                                   |
| ------------------------ | ----------------------------------------------------------------------------------------------- |
| `confirmation.completed` | Approuvé et débité. Contient `transactionId`. **Finalisez votre action ici**                    |
| `confirmation.failed`    | Impossible à finaliser — solde insuffisant ou erreur interne. Contient `failureReason`          |
| `confirmation.expired`   | Non confirmé dans les 10 minutes. Aucun Vito déplacé                                            |
| `confirmation.cancelled` | Annulé par l'utilisateur. Aucun Vito déplacé                                                    |
| `credit.completed`       | Un `/add` financé par le détenteur a été réglé. Émis immédiatement — sans étape de confirmation |

<Note>
  Les webhooks sont réessayés **5 fois avec temporisation croissante**. Répondez rapidement en 2xx et effectuez votre livraison de manière asynchrone.
</Note>

## Codes d'erreur

Chaque réponse est encapsulée. Un succès porte `data`, un échec porte `error`, jamais les deux :

```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>
  **Tous les codes sont préfixés par `VITO_`.** Comparez la chaîne complète — un `RATE_LIMITED` ou `FORBIDDEN` isolé n'apparaît jamais dans les réponses.
</Warning>

| Code                                  | Statut | Signification                                                                    |
| ------------------------------------- | ------ | -------------------------------------------------------------------------------- |
| `VITO_VALIDATION_ERROR`               | 400    | Un champ est manquant ou mal formé                                               |
| `VITO_INVALID_AMOUNT`                 | 400    | `amount` n'est pas un entier positif                                             |
| `VITO_AMOUNT_LIMIT_EXCEEDED`          | 400    | Au-delà de votre plafond par transaction                                         |
| `VITO_DAILY_VOLUME_EXCEEDED`          | 400    | Cet appel dépasserait votre plafond de volume quotidien                          |
| `VITO_SELF_TRANSFER`                  | 400    | L'expéditeur et le destinataire sont le même utilisateur                         |
| `VITO_INVALID_API_KEY`                | 401    | Clé absente, mal formée, révoquée ou inconnue                                    |
| `VITO_INSUFFICIENT_FUNDS`             | 402    | L'utilisateur ne peut pas couvrir le débit                                       |
| `VITO_INSUFFICIENT_OWNER_FUNDS`       | 402    | **Votre** solde est trop faible pour financer un `/add`                          |
| `VITO_INSUFFICIENT_SCOPE`             | 403    | La clé n'a pas le scope requis par cet endpoint                                  |
| `VITO_IP_NOT_ALLOWED`                 | 403    | L'IP appelante n'est pas dans la liste autorisée                                 |
| `VITO_OWNER_MEMBERSHIP_INACTIVE`      | 403    | L'abonnement du détenteur a expiré — gelé jusqu'au renouvellement                |
| `VITO_GUILD_NOT_ALLOWED`              | 403    | `guildId` ne figure pas dans la liste des serveurs autorisés du projet           |
| `VITO_TOS_NOT_ACCEPTED`               | 403    | Conditions développeur non acceptées, ou une version plus récente est en attente |
| `VITO_PROJECT_FROZEN`                 | 403    | Gelé — le plus souvent un abonnement du détenteur expiré                         |
| `VITO_PROJECT_SUSPENDED`              | 403    | Suspendu par l'équipe Vetox                                                      |
| `VITO_PROJECT_BANNED`                 | 403    | Banni par l'équipe Vetox                                                         |
| `VITO_USER_BLACKLISTED`               | 403    | L'utilisateur est exclu des opérations Vito                                      |
| `VITO_ACCOUNT_LOCKED`                 | 403    | Le portefeuille de l'utilisateur est verrouillé après des échecs de PIN          |
| `VITO_USER_NOT_FOUND`                 | 404    | Aucun compte Vito pour cet identifiant Discord                                   |
| `VITO_CONFIRMATION_NOT_FOUND`         | 404    | Jeton de confirmation inconnu                                                    |
| `VITO_IDEMPOTENCY_CONFLICT`           | 409    | Même `Idempotency-Key`, corps différent                                          |
| `VITO_IDEMPOTENCY_IN_PROGRESS`        | 409    | Une requête identique est encore en cours — réessayez sous peu                   |
| `VITO_CONFIRMATION_ALREADY_PROCESSED` | 409    | Cette confirmation a déjà atteint un état final                                  |
| `VITO_KEY_ROTATION_IN_PROGRESS`       | 409    | Une rotation est déjà en cours                                                   |
| `VITO_CONFIRMATION_EXPIRED`           | 410    | La fenêtre de 10 minutes est écoulée                                             |
| `VITO_RATE_LIMITED`                   | 429    | Ralentissez et réessayez                                                         |
| `VITO_INTERNAL_ERROR`                 | 500    | Défaillance inattendue de notre côté                                             |

## Limites de débit

| Abonnement du détenteur | Par minute | Par heure |
| ----------------------- | ---------- | --------- |
| Aucun                   | 60         | 1 000     |
| Silver ou Gold          | 180        | 5 000     |
| Platinum ou Diamond     | 600        | 15 000    |

Il existe aussi une limite par IP égale à la moitié de votre quota par minute, avec un plancher de 30.

<Warning>
  En charge, **les endpoints d'écriture échouent en mode fermé** — un débit est rejeté plutôt que de risquer une double dépense. Les lectures échouent en mode ouvert. Traitez une écriture rejetée comme « n'a pas eu lieu » et réessayez.
</Warning>

<Note>
  Envoyez un en-tête `Idempotency-Key` pour dédupliquer les nouvelles tentatives en toute sécurité.
</Note>

## Endpoints

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

## Limites

* Les confirmations expirent au bout de **10 minutes** — considérez les demandes non confirmées comme abandonnées
* `amount` doit être un entier positif
* `metadata` est plafonné à 10 clés
* Les plafonds par transaction et quotidiens sont fixés par l'équipe Vetox et affichés en lecture seule dans l'onglet des paramètres

## Check-list de sécurité

<AccordionGroup>
  <Accordion title="Gardez les secrets côté serveur" icon="lock">
    La clé API et le secret de signature n'ont jamais leur place dans du code client. En cas de fuite de l'un ou l'autre, effectuez immédiatement une rotation.
  </Accordion>

  <Accordion title="Vérifiez chaque webhook" icon="signature">
    Contrôlez la signature sur le corps brut et rejetez les livraisons datant de plus de \~5 minutes.
  </Accordion>

  <Accordion title="Ne réglez que sur completed" icon="circle-check">
    Ne livrez jamais sur la seule réponse de `/deduct` — le débit n'est définitif qu'avec `confirmation.completed`.
  </Accordion>

  <Accordion title="Moindre privilège" icon="key">
    Ne demandez que les scopes réellement utilisés et activez la liste d'IP autorisées.
  </Accordion>
</AccordionGroup>

## Dépannage

<AccordionGroup>
  <Accordion title="Tous les appels renvoient non autorisé">
    L'abonnement du détenteur a expiré. Il est revérifié à chaque appel.
  </Accordion>

  <Accordion title="J'ai perdu ma clé">
    Elle est irrécupérable — seul un hachage est stocké. Effectuez une rotation pour en obtenir une nouvelle.
  </Accordion>

  <Accordion title="Les signatures de webhook échouent après une rotation">
    Acceptez les deux secrets pendant le chevauchement de 24 heures.
  </Accordion>

  <Accordion title="Un débit ne se finalise jamais">
    L'utilisateur ne l'a pas approuvé. Les confirmations expirent au bout de 10 minutes.
  </Accordion>

  <Accordion title="Aucun webhook n'arrive">
    Un projet a besoin d'une URL de rappel et d'un secret de signature. Avec un seul des deux, rien n'est livré.
  </Accordion>

  <Accordion title="J'ai reçu moins de Vito que ce que j'ai débité">
    Ce sont les frais de règlement. Utilisez des montants de 5 Vito ou moins pour les éviter, ou intégrez-les à votre prix.
  </Accordion>

  <Accordion title="402 VITO_INSUFFICIENT_OWNER_FUNDS">
    `/v1/add` est financé sur votre propre solde, il n'est pas créé à partir de rien. Rechargez.
  </Accordion>
</AccordionGroup>

<CardGroup cols={2}>
  <Card title="Vito" icon="coins" href="/fr/members/vito">
    Soldes, code PIN et frais.
  </Card>

  <Card title="Demandes de paiement" icon="receipt" href="/fr/account/payment-requests">
    Ce que voit l'utilisateur quand vous le débitez.
  </Card>
</CardGroup>
