> ## 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 und Käufe

> Erlaube geprüften Apps, dein Vito mit PIN-Bestätigung zu belasten, und verwalte Zahlungsanfragen auf deiner Käufe-Seite.

<Info>
  Erfordert eine **Silver**-Mitgliedschaft oder höher — sowohl für die Bewerbung als auch für jeden authentifizierten Aufruf. Läuft die Mitgliedschaft des Schlüsselinhabers aus, wird das Projekt eingefroren, bis er sie erneuert.
</Info>

Eine REST-API, mit der deine Anwendung das [Vito](/de/members/vito)-Guthaben eines Nutzers aus Discord heraus verwendet — lesen, belasten, gutschreiben oder zwischen Nutzern bewegen. Alle Endpunkte liefern JSON und sind unter `/v1` versioniert.

<Warning>
  **Vito bewegt sich nie zu oder von echtem Geld.** Es wandert ausschließlich zwischen Vetox-Guthaben.
</Warning>

## Zugang erhalten

Der Zugang wird **pro Projekt** vergeben. Du brauchst alle vier:

<Steps>
  <Step title="Eine aktive Mitgliedschaft, Silver oder höher">
    Wird bei jedem Aufruf geprüft, nicht nur bei der Genehmigung.
  </Step>

  <Step title="Eine genehmigte Entwicklerbewerbung">
    Wird auf der Vito-API-Seite im Dashboard eingereicht und vom Vetox-Team manuell geprüft.
  </Step>

  <Step title="Akzeptierte API-Entwicklerbedingungen">
    Werden beim Einreichen der Bewerbung bestätigt.
  </Step>

  <Step title="Die Scopes, die dein Projekt benötigt">
    Werden vom Vetox-Team anhand deiner Beschreibung vergeben.
  </Step>
</Steps>

<Tip>
  Sei konkret, was du baust und wie du den Schlüssel speicherst. Vage Bewerbungen sind die, die abgelehnt werden.
</Tip>

### Scopes

| Scope               | Erlaubt                                                                |
| ------------------- | ---------------------------------------------------------------------- |
| `balance:read`      | Das Vito-Guthaben eines Nutzers lesen                                  |
| `deduct:create`     | Das Guthaben eines Nutzers belasten                                    |
| `credit:create`     | Einem Nutzer Vito gutschreiben, finanziert aus deinem eigenen Guthaben |
| `transfer:create`   | Vito zwischen zwei Nutzern bewegen                                     |
| `transactions:read` | Transaktionen deines Projekts auflisten und lesen                      |

## Authentifizierung

Sende deinen geheimen Schlüssel als Bearer-Token:

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

Zwei optionale Ebenen härten ein Projekt zusätzlich ab:

* **IP-Allowlist** — Aufrufe auf bestimmte Server-IPs beschränken
* **Rate-Limits** — Obergrenzen pro Projekt, die mit der Mitgliedschaftsstufe des Inhabers steigen

### Schlüssel, Rotation und Speicherung

<Warning>
  **Dein Schlüssel und dein Signing Secret werden genau einmal angezeigt.** Nach der Genehmigung hast du ein **7-Tage-Fenster**, um sie im Tab „API Keys“ aufzudecken. Vetox speichert nur einen Hash und kann sie nicht erneut anzeigen — verpasst du das Fenster, musst du rotieren.
</Warning>

* Halte ihn **ausschließlich serverseitig** — wer ihn besitzt, kann deine Nutzer belasten
* Rotiere im Tab „API Keys“. Der vorherige Schlüssel funktioniert weitere **24 Stunden** als Karenzzeit, damit du ohne Ausfall deployen kannst
* Das Webhook-Signing-Secret rotiert separat, mit eigener 24-Stunden-Überlappung
* Bei einem Leak sofort rotieren

## Einen Nutzer belasten

<Warning>
  **Dein Schlüssel allein kann das Vito eines Nutzers nicht bewegen.** Jede Belastung erfordert die Freigabe des Nutzers mit seiner Wallet-PIN, auf `vetox.io` — niemals in deiner App und niemals in Discord.
</Warning>

<Steps>
  <Step title="Deine App ruft POST /v1/deduct auf">
    Mit dem Nutzer, dem Betrag, der auslösenden `guildId` und den Artikeldetails.
  </Step>

  <Step title="Vito liefert eine confirmUrl">
    Eine ausstehende Bestätigung, **10 Minuten** gültig. Der Nutzer erhält zusätzlich eine DM.
  </Step>

  <Step title="Der Nutzer bestätigt mit seiner PIN">
    Auf `vetox.io`.
  </Step>

  <Step title="Vito bucht und benachrichtigt">
    Das Guthaben wird belastet, die Transaktion gespeichert und ein signierter Webhook gesendet, sofern konfiguriert.
  </Step>

  <Step title="Deine App prüft und schließt ab">
    Signatur prüfen, dann den Inhalt freischalten oder den Artikel ausliefern.
  </Step>
</Steps>

<Warning>
  **Schließe deine Aktion ausschließlich bei `confirmation.completed` ab** — niemals bei der Antwort von `/deduct`. Zu diesem Zeitpunkt ist die Belastung noch nicht endgültig.
</Warning>

### Request-Parameter — `/v1/deduct`

| Feld          | Erforderlich | Hinweise                                                                                       |
| ------------- | ------------ | ---------------------------------------------------------------------------------------------- |
| `discordId`   | **Ja**       | Die Discord-ID des Nutzers — ein Snowflake mit 17–20 Ziffern                                   |
| `amount`      | **Ja**       | Positive Ganzzahl                                                                              |
| `guildId`     | **Ja**       | Der Discord-Server, von dem die Belastung ausgeht. Jede Belastung muss von einem Server kommen |
| `reason`      | Nein         | Bis zu 256 Zeichen. Wird dem Nutzer angezeigt und im Webhook zurückgegeben                     |
| `merchantRef` | Nein         | Deine eigene Referenz, bis zu 128 Zeichen. Damit ordnest du den Webhook deinen Datensätzen zu  |
| `product`     | Nein         | `{ type, name, description?, imageUrl? }` — erscheint auf der Bestätigungsseite und in der DM  |
| `imageUrl`    | Nein         | Muss `https://` sein                                                                           |
| `metadata`    | Nein         | Bis zu **10** String-Schlüssel/Wert-Paare, unverändert durchgereicht                           |

## Einem Nutzer gutschreiben

`POST /v1/add` schreibt einem Nutzer Vito **aus deinem eigenen Guthaben** gut — für Belohnungen oder Erstattungen. Dieselben Felder wie `/deduct`, außer `guildId` und `product`.

<Note>
  Anders als eine Belastung hat eine Gutschrift **keinen Bestätigungsschritt** — sie wird sofort gebucht. Erfordert den Scope `credit:create` und ausreichendes Guthaben, sonst antwortet der Aufruf mit **402 `VITO_INSUFFICIENT_OWNER_FUNDS`**.
</Note>

## Gebühren

Jede Belastung wird an dich ausgezahlt, abzüglich der Plattformgebühr — nach demselben Schema wie In-App-Vito-Transfers, basierend auf **deiner** Mitgliedschaftsstufe:

| Deine Mitgliedschaft | Gebühr |
| -------------------- | ------ |
| Normal, Silver, Gold | 7 %    |
| Platinum             | 6 %    |
| Diamond              | 5 %    |

<Note>
  Beträge von **5 Vito oder weniger sind gebührenfrei**, und Gutschriften über `/v1/add` sind immer gebührenfrei.
</Note>

## Webhooks

Trage im Tab „Settings“ eine oder mehrere `https`-Callback-URLs ein. Vito sendet ein signiertes `POST`, sobald eine Bestätigung einen Endzustand erreicht.

<Warning>
  Webhooks werden nur ausgeliefert, wenn dein Projekt **sowohl** eine Callback-URL **als auch** ein Signing Secret hat. Decke das Secret (`whsec_…`) einmalig im Tab „API Keys“ auf.
</Warning>

### Signatur prüfen

Jede Auslieferung trägt einen `X-Vito-Signature`-Header:

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

Zwei weitere Header begleiten jede Auslieferung — nutze `X-Vito-Event-Id` als Deduplizierungsschlüssel, denn ein Retry sendet dieselbe ID erneut:

| Header              | Enthält                                                     |
| ------------------- | ----------------------------------------------------------- |
| `X-Vito-Event-Id`   | Stabile ID für dieses Event — über Retries hinweg identisch |
| `X-Vito-Event-Type` | z. B. `confirmation.completed`                              |

<Warning>
  **Während einer Signing-Secret-Rotation enthält der Header mehr als eine Signatur**, die neueste zuerst:

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

  Akzeptiere die Auslieferung, wenn **irgendein** `h1` passt. Ein Verifier, der nur den ersten liest, lehnt jeden Webhook ab, bis er das neue Secret ausgerollt hat — was den Sinn der 24-Stunden-Überlappung zunichtemacht.
</Warning>

Berechne den HMAC über `<ts>:<rawBody>` mit deinem Signing Secret neu und vergleiche in konstanter Zeit.

```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>
  Prüfe gegen den **Raw Body**, bevor JSON-Parsing oder Middleware ihn umschreibt.
</Warning>

### Events

Fünf Event-Typen, alle mit derselben Payload-Struktur. `data.status` trägt das Ergebnis.

| Event                    | Bedeutung                                                                                                 |
| ------------------------ | --------------------------------------------------------------------------------------------------------- |
| `confirmation.completed` | Bestätigt und belastet. Enthält `transactionId`. **Hier deine Aktion abschließen**                        |
| `confirmation.failed`    | Nicht abschließbar — zu wenig Guthaben oder interner Fehler. Enthält `failureReason`                      |
| `confirmation.expired`   | Nicht innerhalb von 10 Minuten bestätigt. Kein Vito bewegt                                                |
| `confirmation.cancelled` | Vom Nutzer abgebrochen. Kein Vito bewegt                                                                  |
| `credit.completed`       | Eine vom Inhaber finanzierte `/add`-Gutschrift wurde gebucht. Sofort ausgelöst — ohne Bestätigungsschritt |

<Note>
  Webhooks werden **5-mal mit Backoff** wiederholt. Bestätige schnell mit 2xx und erledige deine Auslieferung asynchron.
</Note>

## Fehlercodes

Jede Antwort ist in einen Envelope verpackt. Ein Erfolg trägt `data`, ein Fehler `error` — nie beides:

```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>
  **Jeder Code beginnt mit `VITO_`.** Vergleiche den vollständigen String — ein blankes `RATE_LIMITED` oder `FORBIDDEN` erscheint nie auf der Leitung.
</Warning>

| Code                                  | Status | Bedeutung                                                                       |
| ------------------------------------- | ------ | ------------------------------------------------------------------------------- |
| `VITO_VALIDATION_ERROR`               | 400    | Ein Feld fehlt oder ist fehlerhaft                                              |
| `VITO_INVALID_AMOUNT`                 | 400    | `amount` ist keine positive Ganzzahl                                            |
| `VITO_AMOUNT_LIMIT_EXCEEDED`          | 400    | Über deinem Limit pro Transaktion                                               |
| `VITO_DAILY_VOLUME_EXCEEDED`          | 400    | Dieser Aufruf würde dein Tageslimit sprengen                                    |
| `VITO_SELF_TRANSFER`                  | 400    | Sender und Empfänger sind derselbe Nutzer                                       |
| `VITO_INVALID_API_KEY`                | 401    | Fehlender, fehlerhafter, widerrufener oder unbekannter Schlüssel                |
| `VITO_INSUFFICIENT_FUNDS`             | 402    | Der Nutzer kann die Belastung nicht decken                                      |
| `VITO_INSUFFICIENT_OWNER_FUNDS`       | 402    | **Dein** Guthaben reicht nicht für ein `/add`                                   |
| `VITO_INSUFFICIENT_SCOPE`             | 403    | Dem Schlüssel fehlt der nötige Scope                                            |
| `VITO_IP_NOT_ALLOWED`                 | 403    | Die aufrufende IP steht nicht auf der Allowlist                                 |
| `VITO_OWNER_MEMBERSHIP_INACTIVE`      | 403    | Die Mitgliedschaft des Inhabers ist abgelaufen — eingefroren bis zur Erneuerung |
| `VITO_GUILD_NOT_ALLOWED`              | 403    | `guildId` steht nicht auf der Server-Allowlist des Projekts                     |
| `VITO_TOS_NOT_ACCEPTED`               | 403    | Entwicklerbedingungen nicht akzeptiert oder eine neuere Version steht aus       |
| `VITO_PROJECT_FROZEN`                 | 403    | Eingefroren — meist eine abgelaufene Mitgliedschaft des Inhabers                |
| `VITO_PROJECT_SUSPENDED`              | 403    | Vom Vetox-Team gesperrt                                                         |
| `VITO_PROJECT_BANNED`                 | 403    | Vom Vetox-Team gebannt                                                          |
| `VITO_USER_BLACKLISTED`               | 403    | Der Nutzer ist von Vito-Vorgängen ausgeschlossen                                |
| `VITO_ACCOUNT_LOCKED`                 | 403    | Die Wallet des Nutzers ist nach fehlgeschlagenen PIN-Versuchen gesperrt         |
| `VITO_USER_NOT_FOUND`                 | 404    | Kein Vito-Konto für diese Discord-ID                                            |
| `VITO_CONFIRMATION_NOT_FOUND`         | 404    | Unbekanntes Bestätigungs-Token                                                  |
| `VITO_IDEMPOTENCY_CONFLICT`           | 409    | Gleicher `Idempotency-Key`, anderer Body                                        |
| `VITO_IDEMPOTENCY_IN_PROGRESS`        | 409    | Eine identische Anfrage läuft noch — kurz darauf erneut versuchen               |
| `VITO_CONFIRMATION_ALREADY_PROCESSED` | 409    | Diese Bestätigung hat bereits einen Endzustand erreicht                         |
| `VITO_KEY_ROTATION_IN_PROGRESS`       | 409    | Eine Rotation läuft bereits                                                     |
| `VITO_CONFIRMATION_EXPIRED`           | 410    | Das 10-Minuten-Fenster ist abgelaufen                                           |
| `VITO_RATE_LIMITED`                   | 429    | Zurückfahren und erneut versuchen                                               |
| `VITO_INTERNAL_ERROR`                 | 500    | Unerwarteter Fehler auf unserer Seite                                           |

## Rate-Limits

| Mitgliedschaft des Inhabers | Pro Minute | Pro Stunde |
| --------------------------- | ---------- | ---------- |
| Keine                       | 60         | 1.000      |
| Silver oder Gold            | 180        | 5.000      |
| Platinum oder Diamond       | 600        | 15.000     |

Zusätzlich gilt ein Limit pro IP in Höhe der halben Minutenquote, mindestens jedoch 30.

<Warning>
  Unter Last **scheitern Schreib-Endpunkte geschlossen** — eine Belastung wird abgelehnt, statt ein Double-Spend zu riskieren. Lesende Endpunkte scheitern offen. Behandle eine abgelehnte Schreiboperation als „nicht passiert“ und wiederhole sie.
</Warning>

<Note>
  Sende einen `Idempotency-Key`-Header, um Retries sicher zu deduplizieren.
</Note>

## Endpunkte

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

## Grenzwerte

* Bestätigungen verfallen nach **10 Minuten** — behandle unbestätigte Anfragen als abgebrochen
* `amount` muss eine positive Ganzzahl sein
* `metadata` ist auf 10 Schlüssel begrenzt
* Limits pro Transaktion und pro Tag legt das Vetox-Team fest; sie erscheinen schreibgeschützt im Tab „Settings“

## Sicherheits-Checkliste

<AccordionGroup>
  <Accordion title="Secrets serverseitig halten" icon="lock">
    API-Schlüssel und Signing Secret gehören niemals in Client-Code. Bei einem Leak sofort rotieren.
  </Accordion>

  <Accordion title="Jeden Webhook prüfen" icon="signature">
    Signatur gegen den Raw Body prüfen und Auslieferungen älter als \~5 Minuten ablehnen.
  </Accordion>

  <Accordion title="Nur bei completed abschließen" icon="circle-check">
    Niemals auf die `/deduct`-Antwort hin ausliefern — die Belastung ist erst mit `confirmation.completed` endgültig.
  </Accordion>

  <Accordion title="Least Privilege" icon="key">
    Fordere nur die Scopes an, die du wirklich nutzt, und aktiviere die IP-Allowlist.
  </Accordion>
</AccordionGroup>

## Fehlerbehebung

<AccordionGroup>
  <Accordion title="Jeder Aufruf liefert „nicht autorisiert“">
    Die Mitgliedschaft des Inhabers ist abgelaufen. Sie wird bei jedem Aufruf erneut geprüft.
  </Accordion>

  <Accordion title="Ich habe meinen Schlüssel verloren">
    Er lässt sich nicht wiederherstellen — gespeichert wird nur ein Hash. Rotiere für einen neuen.
  </Accordion>

  <Accordion title="Webhook-Signaturen scheitern nach der Rotation">
    Akzeptiere während der 24-stündigen Überlappung beide Secrets.
  </Accordion>

  <Accordion title="Eine Belastung wird nie abgeschlossen">
    Der Nutzer hat sie nicht freigegeben. Bestätigungen verfallen nach 10 Minuten.
  </Accordion>

  <Accordion title="Es kommen keine Webhooks an">
    Ein Projekt braucht Callback-URL und Signing Secret. Mit nur einem von beiden wird nichts ausgeliefert.
  </Accordion>

  <Accordion title="Es kam weniger Vito an, als ich belastet habe">
    Das ist die Abrechnungsgebühr. Nutze Beträge von 5 Vito oder weniger, um sie zu vermeiden, oder kalkuliere sie ein.
  </Accordion>

  <Accordion title="402 VITO_INSUFFICIENT_OWNER_FUNDS">
    `/v1/add` wird aus deinem eigenen Guthaben finanziert, nicht aus dem Nichts erzeugt. Lade auf.
  </Accordion>
</AccordionGroup>

<CardGroup cols={2}>
  <Card title="Vito" icon="coins" href="/de/members/vito">
    Guthaben, PIN und Gebühren.
  </Card>

  <Card title="Zahlungsanfragen" icon="receipt" href="/de/account/payment-requests">
    Was der Nutzer sieht, wenn du ihn belastest.
  </Card>
</CardGroup>
