> ## 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 dan Pembelian

> Biarkan aplikasi yang disetujui menagih Vito Anda dengan konfirmasi PIN, dan kelola permintaan pembayaran dari halaman Purchases Anda.

<Info>
  Membutuhkan Keanggotaan **Silver** atau lebih tinggi — baik untuk mendaftar maupun untuk setiap panggilan terautentikasi. Jika Keanggotaan pemilik kunci berakhir, proyek dibekukan sampai ia berlangganan lagi.
</Info>

REST API yang memungkinkan aplikasimu bekerja dengan saldo [Vito](/id/members/vito) seorang pengguna dari dalam Discord — membacanya, menagihnya, menambahkannya, atau memindahkannya antar pengguna. Semua endpoint mengembalikan JSON dan diberi versi di bawah `/v1`.

<Warning>
  **Vito tidak pernah berpindah ke atau dari uang sungguhan.** Ia hanya bergerak antar saldo Vetox.
</Warning>

## Mendapatkan akses

Akses diberikan **per proyek**. Kamu memerlukan keempatnya:

<Steps>
  <Step title="Keanggotaan aktif, Silver atau lebih tinggi">
    Diperiksa pada setiap panggilan, bukan hanya saat persetujuan.
  </Step>

  <Step title="Pengajuan pengembang yang disetujui">
    Dikirim dari halaman Vito API di dasbormu. Ditinjau secara manual oleh tim Vetox.
  </Step>

  <Step title="Ketentuan Pengembang API yang disetujui">
    Disetujui saat kamu mengirim pengajuan.
  </Step>

  <Step title="Scope yang dibutuhkan proyekmu">
    Diberikan oleh tim Vetox berdasarkan apa yang kamu jelaskan.
  </Step>
</Steps>

<Tip>
  Jelaskan secara konkret apa yang kamu bangun dan bagaimana kamu menyimpan kuncinya. Pengajuan yang samar-samar itulah yang ditolak.
</Tip>

### Scope

| Scope               | Memberi izin                                               |
| ------------------- | ---------------------------------------------------------- |
| `balance:read`      | Membaca saldo Vito seorang pengguna                        |
| `deduct:create`     | Menagih saldo seorang pengguna                             |
| `credit:create`     | Menambahkan Vito ke pengguna, didanai dari saldomu sendiri |
| `transfer:create`   | Memindahkan Vito antara dua pengguna                       |
| `transactions:read` | Menampilkan dan membaca transaksi proyekmu                 |

## Autentikasi

Kirim kunci rahasiamu sebagai token Bearer:

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

Dua lapisan opsional memperkuat proyek lebih jauh:

* **Daftar IP yang diizinkan** — membatasi panggilan ke IP server tertentu
* **Batas laju** — plafon per proyek yang naik seiring tingkat Keanggotaan pemilik

### Kunci, rotasi, dan penyimpanan

<Warning>
  **Kunci dan secret penandatanganan ditampilkan tepat satu kali.** Setelah disetujui kamu punya **jendela 7 hari** untuk mengungkapkannya di tab kunci API. Vetox hanya menyimpan hash dan tidak bisa menampilkannya lagi — jika terlewat, kamu harus melakukan rotasi.
</Warning>

* Simpan **hanya di sisi server** — siapa pun yang memegangnya bisa menagih penggunamu
* Rotasi dari tab kunci API. Kunci sebelumnya tetap berfungsi selama **masa tenggang 24 jam** agar kamu bisa merilis tanpa waktu henti
* Secret penandatanganan webhook dirotasi terpisah, dengan tumpang tindih 24 jam tersendiri
* Jika bocor, segera rotasi

## Menagih pengguna

<Warning>
  **Kuncimu saja tidak bisa memindahkan Vito seorang pengguna.** Setiap penagihan mengharuskan pengguna menyetujuinya dengan PIN dompetnya, di `vetox.io` — tidak pernah di dalam aplikasimu maupun di dalam Discord.
</Warning>

<Steps>
  <Step title="Aplikasimu memanggil POST /v1/deduct">
    Dengan pengguna, jumlah, `guildId` asal, dan detail barang.
  </Step>

  <Step title="Vito mengembalikan confirmUrl">
    Konfirmasi tertunda, berlaku **10 menit**. Pengguna juga menerima DM.
  </Step>

  <Step title="Pengguna menyetujui dengan PIN-nya">
    Di `vetox.io`.
  </Step>

  <Step title="Vito menyelesaikan dan memberi tahu">
    Saldo dipotong, transaksi dicatat, dan webhook bertanda tangan dikirim jika kamu sudah mengaturnya.
  </Step>

  <Step title="Aplikasimu memverifikasi dan menyelesaikan">
    Periksa tanda tangannya, lalu buka kontennya atau kirim barangnya.
  </Step>
</Steps>

<Warning>
  **Selesaikan tindakanmu hanya pada `confirmation.completed`** — jangan pernah pada respons `/deduct`. Pada titik itu penagihan belum final.
</Warning>

### Parameter permintaan — `/v1/deduct`

| Bidang        | Wajib  | Catatan                                                                                      |
| ------------- | ------ | -------------------------------------------------------------------------------------------- |
| `discordId`   | **Ya** | ID Discord pengguna — snowflake 17–20 digit                                                  |
| `amount`      | **Ya** | Bilangan bulat positif                                                                       |
| `guildId`     | **Ya** | Server Discord tempat penagihan berasal. Setiap penagihan harus berasal dari sebuah server   |
| `reason`      | Tidak  | Hingga 256 karakter. Ditampilkan ke pengguna dan dikembalikan di webhook                     |
| `merchantRef` | Tidak  | Referensimu sendiri, hingga 128 karakter. Gunakan untuk mencocokkan webhook dengan catatanmu |
| `product`     | Tidak  | `{ type, name, description?, imageUrl? }` — tampil di halaman konfirmasi dan DM              |
| `imageUrl`    | Tidak  | Harus `https://`                                                                             |
| `metadata`    | Tidak  | Hingga **10** pasangan kunci/nilai teks, diteruskan apa adanya                               |

## Menambah saldo pengguna

`POST /v1/add` menambahkan Vito ke pengguna **dari saldomu sendiri** — untuk hadiah atau pengembalian dana. Bidang yang sama dengan `/deduct` kecuali `guildId` dan `product`.

<Note>
  Berbeda dari penagihan, penambahan **tidak punya langkah konfirmasi** — langsung diselesaikan. Membutuhkan scope `credit:create` dan saldo yang cukup, atau panggilan mengembalikan **402 `VITO_INSUFFICIENT_OWNER_FUNDS`**.
</Note>

## Biaya

Setiap penagihan diselesaikan kepadamu dikurangi biaya platform — skema yang sama dengan transfer Vito di dalam aplikasi, berdasarkan tingkat Keanggotaan **kamu**:

| Keanggotaanmu        | Biaya |
| -------------------- | ----- |
| Normal, Silver, Gold | 7%    |
| Platinum             | 6%    |
| Diamond              | 5%    |

<Note>
  Jumlah **5 Vito atau kurang bebas biaya**, dan penambahan lewat `/v1/add` selalu bebas biaya.
</Note>

## Webhook

Tambahkan satu atau beberapa URL callback `https` di tab pengaturan. Vito mengirim `POST` bertanda tangan setiap kali sebuah konfirmasi mencapai keadaan akhir.

<Warning>
  Webhook hanya terkirim jika proyekmu punya **sekaligus** URL callback **dan** secret penandatanganan. Ungkapkan secret (`whsec_…`) satu kali dari tab kunci API.
</Warning>

### Memverifikasi tanda tangan

Setiap pengiriman membawa header `X-Vito-Signature`:

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

Dua header lain menyertai setiap pengiriman — gunakan `X-Vito-Event-Id` sebagai kunci deduplikasi, karena percobaan ulang mengirim id yang sama:

| Header              | Berisi                                                       |
| ------------------- | ------------------------------------------------------------ |
| `X-Vito-Event-Id`   | Id tetap untuk peristiwa ini — sama di semua percobaan ulang |
| `X-Vito-Event-Type` | Misalnya `confirmation.completed`                            |

<Warning>
  **Selama rotasi secret penandatanganan, header membawa lebih dari satu tanda tangan**, yang terbaru lebih dulu:

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

  Terima pengiriman jika **salah satu** `h1` cocok. Verifikator yang hanya membaca yang pertama akan menolak setiap webhook sampai ia merilis secret baru — dan itu justru meniadakan seluruh tujuan tumpang tindih 24 jam.
</Warning>

Hitung ulang HMAC atas `<ts>:<rawBody>` dengan secret penandatangananmu dan bandingkan dalam waktu konstan.

```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>
  Verifikasi terhadap **body mentah**, sebelum parsing JSON atau middleware menulis ulangnya.
</Warning>

### Peristiwa

Lima jenis peristiwa, semuanya berbagi satu bentuk payload. `data.status` membawa hasilnya.

| Peristiwa                | Arti                                                                                               |
| ------------------------ | -------------------------------------------------------------------------------------------------- |
| `confirmation.completed` | Disetujui dan ditagih. Membawa `transactionId`. **Selesaikan tindakanmu di sini**                  |
| `confirmation.failed`    | Tidak bisa diselesaikan — saldo kurang atau galat internal. Membawa `failureReason`                |
| `confirmation.expired`   | Tidak dikonfirmasi dalam 10 menit. Tidak ada Vito yang berpindah                                   |
| `confirmation.cancelled` | Pengguna membatalkan. Tidak ada Vito yang berpindah                                                |
| `credit.completed`       | Sebuah `/add` yang didanai pemilik telah diselesaikan. Langsung terpicu — tanpa langkah konfirmasi |

<Note>
  Webhook dicoba ulang **5 kali dengan jeda menaik**. Balas 2xx dengan cepat dan lakukan pemenuhanmu secara asinkron.
</Note>

## Kode galat

Setiap respons dibungkus. Keberhasilan membawa `data`, kegagalan membawa `error`, tidak pernah keduanya:

```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>
  **Setiap kode berawalan `VITO_`.** Cocokkan seluruh stringnya — `RATE_LIMITED` atau `FORBIDDEN` polos tidak pernah muncul di respons.
</Warning>

| Kode                                  | Status | Arti                                                                          |
| ------------------------------------- | ------ | ----------------------------------------------------------------------------- |
| `VITO_VALIDATION_ERROR`               | 400    | Sebuah bidang hilang atau salah format                                        |
| `VITO_INVALID_AMOUNT`                 | 400    | `amount` bukan bilangan bulat positif                                         |
| `VITO_AMOUNT_LIMIT_EXCEEDED`          | 400    | Melebihi plafon per transaksimu                                               |
| `VITO_DAILY_VOLUME_EXCEEDED`          | 400    | Panggilan ini akan melewati plafon volume harianmu                            |
| `VITO_SELF_TRANSFER`                  | 400    | Pengirim dan penerima adalah pengguna yang sama                               |
| `VITO_INVALID_API_KEY`                | 401    | Kunci hilang, salah bentuk, dicabut, atau tidak dikenal                       |
| `VITO_INSUFFICIENT_FUNDS`             | 402    | Saldo pengguna tidak mencukupi penagihan                                      |
| `VITO_INSUFFICIENT_OWNER_FUNDS`       | 402    | Saldo **kamu** terlalu rendah untuk mendanai `/add`                           |
| `VITO_INSUFFICIENT_SCOPE`             | 403    | Kunci tidak punya scope yang diminta endpoint                                 |
| `VITO_IP_NOT_ALLOWED`                 | 403    | IP pemanggil tidak ada di daftar yang diizinkan                               |
| `VITO_OWNER_MEMBERSHIP_INACTIVE`      | 403    | Keanggotaan pemilik berakhir — dibekukan sampai berlangganan lagi             |
| `VITO_GUILD_NOT_ALLOWED`              | 403    | `guildId` tidak ada di daftar server yang diizinkan proyek                    |
| `VITO_TOS_NOT_ACCEPTED`               | 403    | Ketentuan pengembang belum disetujui, atau ada versi lebih baru yang menunggu |
| `VITO_PROJECT_FROZEN`                 | 403    | Dibekukan — biasanya karena Keanggotaan pemilik berakhir                      |
| `VITO_PROJECT_SUSPENDED`              | 403    | Ditangguhkan oleh tim Vetox                                                   |
| `VITO_PROJECT_BANNED`                 | 403    | Diblokir oleh tim Vetox                                                       |
| `VITO_USER_BLACKLISTED`               | 403    | Pengguna dilarang melakukan operasi Vito                                      |
| `VITO_ACCOUNT_LOCKED`                 | 403    | Dompet pengguna terkunci setelah percobaan PIN yang gagal                     |
| `VITO_USER_NOT_FOUND`                 | 404    | Tidak ada akun Vito untuk ID Discord itu                                      |
| `VITO_CONFIRMATION_NOT_FOUND`         | 404    | Token konfirmasi tidak dikenal                                                |
| `VITO_IDEMPOTENCY_CONFLICT`           | 409    | `Idempotency-Key` sama, body berbeda                                          |
| `VITO_IDEMPOTENCY_IN_PROGRESS`        | 409    | Permintaan identik masih berjalan — coba lagi sebentar                        |
| `VITO_CONFIRMATION_ALREADY_PROCESSED` | 409    | Konfirmasi itu sudah mencapai keadaan akhir                                   |
| `VITO_KEY_ROTATION_IN_PROGRESS`       | 409    | Rotasi sudah berjalan                                                         |
| `VITO_CONFIRMATION_EXPIRED`           | 410    | Jendela 10 menit sudah lewat                                                  |
| `VITO_RATE_LIMITED`                   | 429    | Kurangi laju lalu coba lagi                                                   |
| `VITO_INTERNAL_ERROR`                 | 500    | Kegagalan tak terduga di sisi kami                                            |

## Batas laju

| Keanggotaan pemilik   | Per menit | Per jam |
| --------------------- | --------- | ------- |
| Tidak ada             | 60        | 1.000   |
| Silver atau Gold      | 180       | 5.000   |
| Platinum atau Diamond | 600       | 15.000  |

Ada juga batas per IP sebesar setengah jatah per menitmu, dengan batas bawah 30.

<Warning>
  Saat beban tinggi, **endpoint tulis gagal secara tertutup** — penagihan ditolak alih-alih mempertaruhkan pembelanjaan ganda. Endpoint baca gagal secara terbuka. Perlakukan penulisan yang ditolak sebagai "tidak terjadi" lalu coba lagi.
</Warning>

<Note>
  Kirim header `Idempotency-Key` untuk menghindari duplikasi saat mencoba ulang.
</Note>

## Endpoint

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

## Batasan

* Konfirmasi kedaluwarsa setelah **10 menit** — anggap permintaan yang belum dikonfirmasi sebagai ditinggalkan
* `amount` harus bilangan bulat positif
* `metadata` dibatasi 10 kunci
* Plafon per transaksi dan harian ditetapkan tim Vetox dan tampil hanya-baca di tab pengaturan

## Daftar periksa keamanan

<AccordionGroup>
  <Accordion title="Simpan secret di sisi server" icon="lock">
    Kunci API dan secret penandatanganan tidak pernah pantas berada di kode klien. Rotasi segera jika salah satunya bocor.
  </Accordion>

  <Accordion title="Verifikasi setiap webhook" icon="signature">
    Periksa tanda tangan terhadap body mentah dan tolak pengiriman yang lebih tua dari \~5 menit.
  </Accordion>

  <Accordion title="Selesaikan hanya pada completed" icon="circle-check">
    Jangan pernah memenuhi berdasarkan respons `/deduct` — penagihan baru final pada `confirmation.completed`.
  </Accordion>

  <Accordion title="Hak seminimal mungkin" icon="key">
    Minta hanya scope yang benar-benar kamu pakai, dan aktifkan daftar IP yang diizinkan.
  </Accordion>
</AccordionGroup>

## Pemecahan masalah

<AccordionGroup>
  <Accordion title="Setiap panggilan mengembalikan tidak terotorisasi">
    Keanggotaan pemilik sudah berakhir. Ia diperiksa ulang pada setiap panggilan.
  </Accordion>

  <Accordion title="Saya kehilangan kunci saya">
    Tidak bisa dipulihkan — hanya hash yang disimpan. Rotasi untuk mendapat yang baru.
  </Accordion>

  <Accordion title="Tanda tangan webhook gagal setelah rotasi">
    Terima kedua secret selama tumpang tindih 24 jam.
  </Accordion>

  <Accordion title="Sebuah penagihan tidak pernah selesai">
    Pengguna tidak menyetujuinya. Konfirmasi kedaluwarsa setelah 10 menit.
  </Accordion>

  <Accordion title="Tidak ada webhook yang datang">
    Sebuah proyek butuh URL callback dan secret penandatanganan. Dengan salah satunya saja, tidak ada yang dikirim.
  </Accordion>

  <Accordion title="Vito yang masuk lebih sedikit dari yang saya tagih">
    Itu biaya penyelesaian. Gunakan jumlah 5 Vito atau kurang untuk menghindarinya, atau perhitungkan dalam hargamu.
  </Accordion>

  <Accordion title="402 VITO_INSUFFICIENT_OWNER_FUNDS">
    `/v1/add` didanai dari saldomu sendiri, bukan diciptakan dari ketiadaan. Isi ulang saldomu.
  </Accordion>
</AccordionGroup>

<CardGroup cols={2}>
  <Card title="Vito" icon="coins" href="/id/members/vito">
    Saldo, PIN, dan biaya.
  </Card>

  <Card title="Permintaan pembayaran" icon="receipt" href="/id/account/payment-requests">
    Apa yang dilihat pengguna saat kamu menagihnya.
  </Card>
</CardGroup>
