> ## 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 en aankopen

> Laat goedgekeurde apps je Vito afboeken met pincode-bevestiging, en beheer betalingsverzoeken vanaf je Aankopen-pagina.

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

Een REST-API waarmee je applicatie vanuit Discord met het [Vito](/nl/members/vito)-saldo van een gebruiker werkt: uitlezen, afschrijven, bijschrijven of tussen gebruikers verplaatsen. Alle endpoints geven JSON terug en zijn geversioneerd onder `/v1`.

<Warning>
  **Vito wordt nooit omgezet in echt geld, en komt er ook nooit uit voort.** Het beweegt uitsluitend tussen Vetox-saldi.
</Warning>

## Toegang krijgen

Toegang wordt **per project** verleend. Je hebt alle vier nodig:

<Steps>
  <Step title="Een actief lidmaatschap, Silver of hoger">
    Wordt bij elke aanroep gecontroleerd, niet alleen bij goedkeuring.
  </Step>

  <Step title="Een goedgekeurde ontwikkelaarsaanvraag">
    Ingediend via de Vito API-pagina in je dashboard. Wordt handmatig beoordeeld door het Vetox-team.
  </Step>

  <Step title="Geaccepteerde API-ontwikkelaarsvoorwaarden">
    Worden bevestigd bij het indienen van de aanvraag.
  </Step>

  <Step title="De scopes die je project nodig heeft">
    Worden door het Vetox-team toegekend op basis van je beschrijving.
  </Step>
</Steps>

<Tip>
  Wees concreet over wat je bouwt en hoe je de sleutel opslaat. Het zijn de vage aanvragen die worden afgewezen.
</Tip>

### Scopes

| Scope               | Staat toe                                                            |
| ------------------- | -------------------------------------------------------------------- |
| `balance:read`      | Het Vito-saldo van een gebruiker uitlezen                            |
| `deduct:create`     | Het saldo van een gebruiker afschrijven                              |
| `credit:create`     | Vito bijschrijven bij een gebruiker, gefinancierd uit je eigen saldo |
| `transfer:create`   | Vito verplaatsen tussen twee gebruikers                              |
| `transactions:read` | Transacties van je project opvragen en lezen                         |

## Authenticatie

Stuur je geheime sleutel mee als Bearer-token:

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

Twee optionele lagen maken een project extra robuust:

* **IP-allowlist** — beperk aanroepen tot specifieke server-IP's
* **Rate limits** — plafonds per project die meeschalen met het lidmaatschapsniveau van de houder

### Sleutels, rotatie en opslag

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

* 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
* Bij een lek: onmiddellijk roteren

## Een gebruiker afschrijven

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

<Steps>
  <Step title="Je app roept POST /v1/deduct aan">
    Met de gebruiker, het bedrag, de `guildId` van herkomst en de artikelgegevens.
  </Step>

  <Step title="Vito geeft een confirmUrl terug">
    Een openstaande bevestiging, **10 minuten** geldig. De gebruiker krijgt ook een DM.
  </Step>

  <Step title="De gebruiker keurt goed met zijn pincode">
    Op `vetox.io`.
  </Step>

  <Step title="Vito verrekent en meldt">
    Het saldo wordt afgeschreven, de transactie vastgelegd en er wordt een ondertekende webhook verstuurd als je er een hebt ingesteld.
  </Step>

  <Step title="Je app verifieert en rondt af">
    Controleer de handtekening en ontgrendel dan de content of lever het artikel.
  </Step>
</Steps>

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

### Requestparameters — `/v1/deduct`

| Veld          | Verplicht | Toelichting                                                                                      |
| ------------- | --------- | ------------------------------------------------------------------------------------------------ |
| `discordId`   | **Ja**    | De Discord-ID van de gebruiker — een snowflake van 17 tot 20 cijfers                             |
| `amount`      | **Ja**    | Positief geheel getal                                                                            |
| `guildId`     | **Ja**    | De Discord-server waar de afschrijving vandaan komt. Elke afschrijving moet van een server komen |
| `reason`      | Nee       | Maximaal 256 tekens. Wordt aan de gebruiker getoond en meegegeven in de webhook                  |
| `merchantRef` | Nee       | Je eigen referentie, maximaal 128 tekens. Handig om de webhook aan je administratie te koppelen  |
| `product`     | Nee       | `{ type, name, description?, imageUrl? }` — verschijnt op de bevestigingspagina en in de DM      |
| `imageUrl`    | Nee       | Moet `https://` zijn                                                                             |
| `metadata`    | Nee       | Maximaal **10** sleutel/waarde-paren als tekst, ongewijzigd doorgegeven                          |

## Een gebruiker bijschrijven

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

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

## Kosten

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

<Note>
  Bedragen van **5 Vito of minder zijn kosteloos**, en bijschrijvingen via `/v1/add` zijn dat altijd.
</Note>

## Webhooks

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.

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

### De handtekening verifiëren

Elke aflevering draagt een `X-Vito-Signature`-header:

```text theme={null}
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`                          |

<Warning>
  **Tijdens een rotatie van het signing secret bevat de header meer dan één handtekening**, de nieuwste eerst:

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

  Accepteer de aflevering als **een willekeurige** `h1` 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.
</Warning>

Herbereken de HMAC over `<ts>:<rawBody>` met je signing secret en vergelijk in constante tijd.

```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>
  Verifieer tegen de **ruwe body**, vóór JSON-parsing of middleware die hem herschrijft.
</Warning>

### Gebeurtenissen

Vijf gebeurtenistypen, allemaal met dezelfde payloadstructuur. `data.status` bevat de uitkomst.

| Gebeurtenis              | Betekenis                                                                                    |
| ------------------------ | -------------------------------------------------------------------------------------------- |
| `confirmation.completed` | Goedgekeurd en afgeschreven. Bevat `transactionId`. **Rond hier je actie af**                |
| `confirmation.failed`    | Kon niet worden afgerond — te weinig saldo of een interne fout. Bevat `failureReason`        |
| `confirmation.expired`   | Niet binnen 10 minuten bevestigd. Er is geen Vito verplaatst                                 |
| `confirmation.cancelled` | De gebruiker heeft geannuleerd. Er is geen Vito verplaatst                                   |
| `credit.completed`       | Een door de houder gefinancierde `/add` is verrekend. Vuurt direct — zonder bevestigingsstap |

<Note>
  Webhooks worden **5 keer met backoff** opnieuw geprobeerd. Bevestig snel met een 2xx en doe je afhandeling asynchroon.
</Note>

## Foutcodes

Elk antwoord zit in een envelope. Een succes bevat `data`, een fout bevat `error` — nooit allebei:

```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>
  **Elke code begint met `VITO_`.** Vergelijk de volledige string — een kale `RATE_LIMITED` of `FORBIDDEN` komt nooit over de lijn.
</Warning>

| Code                                  | Status | Betekenis                                                                        |
| ------------------------------------- | ------ | -------------------------------------------------------------------------------- |
| `VITO_VALIDATION_ERROR`               | 400    | Een veld ontbreekt of is misvormd                                                |
| `VITO_INVALID_AMOUNT`                 | 400    | `amount` is geen positief geheel getal                                           |
| `VITO_AMOUNT_LIMIT_EXCEEDED`          | 400    | Boven je limiet per transactie                                                   |
| `VITO_DAILY_VOLUME_EXCEEDED`          | 400    | Deze aanroep zou je daglimiet overschrijden                                      |
| `VITO_SELF_TRANSFER`                  | 400    | Afzender en ontvanger zijn dezelfde gebruiker                                    |
| `VITO_INVALID_API_KEY`                | 401    | Ontbrekende, misvormde, ingetrokken of onbekende sleutel                         |
| `VITO_INSUFFICIENT_FUNDS`             | 402    | De gebruiker heeft te weinig saldo                                               |
| `VITO_INSUFFICIENT_OWNER_FUNDS`       | 402    | **Jouw** saldo is te laag om een `/add` te financieren                           |
| `VITO_INSUFFICIENT_SCOPE`             | 403    | De sleutel mist de vereiste scope                                                |
| `VITO_IP_NOT_ALLOWED`                 | 403    | Het aanroepende IP staat niet op de allowlist                                    |
| `VITO_OWNER_MEMBERSHIP_INACTIVE`      | 403    | Het lidmaatschap van de houder is verlopen — bevroren tot verlenging             |
| `VITO_GUILD_NOT_ALLOWED`              | 403    | `guildId` staat niet op de serverlijst van het project                           |
| `VITO_TOS_NOT_ACCEPTED`               | 403    | Ontwikkelaarsvoorwaarden niet geaccepteerd, of er staat een nieuwere versie open |
| `VITO_PROJECT_FROZEN`                 | 403    | Bevroren — meestal een verlopen lidmaatschap van de houder                       |
| `VITO_PROJECT_SUSPENDED`              | 403    | Geschorst door het Vetox-team                                                    |
| `VITO_PROJECT_BANNED`                 | 403    | Verbannen door het Vetox-team                                                    |
| `VITO_USER_BLACKLISTED`               | 403    | De gebruiker is uitgesloten van Vito-verrichtingen                               |
| `VITO_ACCOUNT_LOCKED`                 | 403    | De wallet van de gebruiker is vergrendeld na mislukte pinpogingen                |
| `VITO_USER_NOT_FOUND`                 | 404    | Geen Vito-account voor die Discord-ID                                            |
| `VITO_CONFIRMATION_NOT_FOUND`         | 404    | Onbekend bevestigingstoken                                                       |
| `VITO_IDEMPOTENCY_CONFLICT`           | 409    | Dezelfde `Idempotency-Key`, andere body                                          |
| `VITO_IDEMPOTENCY_IN_PROGRESS`        | 409    | Een identiek verzoek loopt nog — probeer het zo opnieuw                          |
| `VITO_CONFIRMATION_ALREADY_PROCESSED` | 409    | Die bevestiging heeft al een eindtoestand bereikt                                |
| `VITO_KEY_ROTATION_IN_PROGRESS`       | 409    | Er loopt al een rotatie                                                          |
| `VITO_CONFIRMATION_EXPIRED`           | 410    | Het venster van 10 minuten is verstreken                                         |
| `VITO_RATE_LIMITED`                   | 429    | Rustig aan en opnieuw proberen                                                   |
| `VITO_INTERNAL_ERROR`                 | 500    | Onverwachte fout aan onze kant                                                   |

## Rate limits

| Lidmaatschap van de houder | Per minuut | Per uur |
| -------------------------- | ---------- | ------- |
| Geen                       | 60         | 1.000   |
| Silver of Gold             | 180        | 5.000   |
| Platinum of Diamond        | 600        | 15.000  |

Er geldt ook een limiet per IP van de helft van je minuutquotum, met een ondergrens van 30.

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

<Note>
  Stuur een `Idempotency-Key`-header mee om herhaalde pogingen veilig te dedupliceren.
</Note>

## Endpoints

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

## Limieten

* Bevestigingen verlopen na **10 minuten** — beschouw niet-bevestigde verzoeken als afgebroken
* `amount` moet een positief geheel getal zijn
* `metadata` is beperkt tot 10 sleutels
* Limieten per transactie en per dag worden door het Vetox-team ingesteld en staan alleen-lezen op het tabblad met instellingen

## Beveiligingschecklist

<AccordionGroup>
  <Accordion title="Houd secrets serverzijdig" icon="lock">
    De API-sleutel en het signing secret horen nooit in clientcode. Roteer meteen als er een lekt.
  </Accordion>

  <Accordion title="Verifieer elke webhook" icon="signature">
    Controleer de handtekening tegen de ruwe body en weiger afleveringen ouder dan \~5 minuten.
  </Accordion>

  <Accordion title="Verreken alleen bij completed" icon="circle-check">
    Lever nooit op basis van het `/deduct`-antwoord — de afschrijving staat pas vast bij `confirmation.completed`.
  </Accordion>

  <Accordion title="Minimale rechten" icon="key">
    Vraag alleen de scopes aan die je echt gebruikt, en zet de IP-allowlist aan.
  </Accordion>
</AccordionGroup>

## Problemen oplossen

<AccordionGroup>
  <Accordion title="Elke aanroep geeft niet-geautoriseerd">
    Het lidmaatschap van de houder is verlopen. Het wordt bij elke aanroep opnieuw gecontroleerd.
  </Accordion>

  <Accordion title="Ik ben mijn sleutel kwijt">
    Die is niet terug te halen — er wordt alleen een hash bewaard. Roteer voor een nieuwe.
  </Accordion>

  <Accordion title="Webhook-handtekeningen falen na rotatie">
    Accepteer beide secrets tijdens de overlap van 24 uur.
  </Accordion>

  <Accordion title="Een afschrijving wordt nooit afgerond">
    De gebruiker heeft niet goedgekeurd. Bevestigingen verlopen na 10 minuten.
  </Accordion>

  <Accordion title="Er komen geen webhooks binnen">
    Een project heeft zowel een callback-URL als een signing secret nodig. Met maar één van beide wordt er niets afgeleverd.
  </Accordion>

  <Accordion title="Er kwam minder Vito binnen dan ik afschreef">
    Dat zijn de verrekeningskosten. Gebruik bedragen van 5 Vito of minder om ze te vermijden, of reken ze door.
  </Accordion>

  <Accordion title="402 VITO_INSUFFICIENT_OWNER_FUNDS">
    `/v1/add` wordt uit je eigen saldo gefinancierd, niet uit het niets gemaakt. Vul aan.
  </Accordion>
</AccordionGroup>

<CardGroup cols={2}>
  <Card title="Vito" icon="coins" href="/nl/members/vito">
    Saldi, de pincode en kosten.
  </Card>

  <Card title="Betaalverzoeken" icon="receipt" href="/nl/account/payment-requests">
    Wat de gebruiker ziet wanneer je hem afschrijft.
  </Card>
</CardGroup>
