> ## 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 và mua hàng

> Cho phép các ứng dụng đã phê duyệt tính phí Vito của bạn với xác nhận PIN và quản lý các yêu cầu thanh toán từ trang Purchases của bạn.

<Info>
  Yêu cầu Hội viên **Silver** trở lên — cả khi đăng ký lẫn cho mọi lệnh gọi đã xác thực. Nếu Hội viên của chủ khóa hết hạn, dự án bị đóng băng cho đến khi họ đăng ký lại.
</Info>

Một REST API cho phép ứng dụng của bạn làm việc với số dư [Vito](/vi/members/vito) của người dùng ngay từ trong Discord — đọc, trừ, cộng, hoặc chuyển giữa những người dùng. Mọi endpoint đều trả về JSON và được đánh phiên bản dưới `/v1`.

<Warning>
  **Vito không bao giờ chuyển sang hay đến từ tiền thật.** Nó chỉ di chuyển giữa các số dư Vetox.
</Warning>

## Nhận quyền truy cập

Quyền truy cập được cấp **theo từng dự án**. Bạn cần cả bốn điều kiện:

<Steps>
  <Step title="Một Hội viên đang hoạt động, Silver trở lên">
    Được kiểm tra ở mọi lệnh gọi, không chỉ lúc phê duyệt.
  </Step>

  <Step title="Một đơn đăng ký nhà phát triển đã được duyệt">
    Gửi từ trang Vito API trong bảng điều khiển của bạn. Đội ngũ Vetox xét duyệt thủ công.
  </Step>

  <Step title="Đã chấp nhận Điều khoản Nhà phát triển API">
    Được xác nhận khi bạn gửi đơn.
  </Step>

  <Step title="Các scope mà dự án của bạn cần">
    Do đội ngũ Vetox cấp dựa trên những gì bạn mô tả.
  </Step>
</Steps>

<Tip>
  Hãy viết cụ thể về thứ bạn đang xây dựng và cách bạn sẽ lưu khóa. Chính những đơn mơ hồ mới bị từ chối.
</Tip>

### Scope

| Scope               | Cho phép                                                 |
| ------------------- | -------------------------------------------------------- |
| `balance:read`      | Đọc số dư Vito của một người dùng                        |
| `deduct:create`     | Trừ vào số dư của một người dùng                         |
| `credit:create`     | Cộng Vito cho một người dùng, lấy từ số dư của chính bạn |
| `transfer:create`   | Chuyển Vito giữa hai người dùng                          |
| `transactions:read` | Liệt kê và đọc các giao dịch của dự án bạn               |

## Xác thực

Gửi khóa bí mật của bạn dưới dạng token Bearer:

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

Hai lớp tùy chọn giúp gia cố dự án thêm nữa:

* **Danh sách IP cho phép** — giới hạn lệnh gọi ở những IP máy chủ nhất định
* **Giới hạn tần suất** — trần theo dự án, tăng dần theo cấp Hội viên của chủ sở hữu

### Khóa, xoay vòng và lưu trữ

<Warning>
  **Khóa và khóa bí mật ký của bạn chỉ hiển thị đúng một lần.** Sau khi được duyệt, bạn có **cửa sổ 7 ngày** để hiện chúng ở tab khóa API. Vetox chỉ lưu một bản băm và không thể hiện lại — bỏ lỡ cửa sổ này thì bạn phải xoay vòng khóa.
</Warning>

* Chỉ giữ nó **ở phía máy chủ** — ai cầm được khóa đều có thể trừ tiền người dùng của bạn
* Xoay vòng từ tab khóa API. Khóa cũ vẫn hoạt động thêm **24 giờ** như thời gian ân hạn, để bạn triển khai mà không gián đoạn
* Khóa bí mật ký webhook được xoay vòng riêng, với khoảng chồng lấn 24 giờ của chính nó
* Nếu bị lộ, hãy xoay vòng ngay

## Trừ tiền một người dùng

<Warning>
  **Chỉ riêng khóa của bạn không thể làm Vito của người dùng dịch chuyển.** Mọi lần trừ tiền đều cần người dùng phê duyệt bằng mã PIN ví của họ, trên `vetox.io` — không bao giờ trong ứng dụng của bạn và không bao giờ trong Discord.
</Warning>

<Steps>
  <Step title="Ứng dụng của bạn gọi POST /v1/deduct">
    Kèm người dùng, số tiền, `guildId` khởi phát và chi tiết mặt hàng.
  </Step>

  <Step title="Vito trả về một confirmUrl">
    Một xác nhận đang chờ, có hiệu lực **10 phút**. Người dùng cũng nhận được tin nhắn riêng.
  </Step>

  <Step title="Người dùng phê duyệt bằng mã PIN">
    Trên `vetox.io`.
  </Step>

  <Step title="Vito quyết toán và thông báo">
    Số dư bị trừ, giao dịch được ghi lại, và một webhook có chữ ký được gửi đi nếu bạn đã cấu hình.
  </Step>

  <Step title="Ứng dụng của bạn xác minh và hoàn tất">
    Kiểm tra chữ ký, rồi mở khóa nội dung hoặc giao mặt hàng.
  </Step>
</Steps>

<Warning>
  **Chỉ hoàn tất hành động của bạn khi có `confirmation.completed`** — đừng bao giờ dựa vào phản hồi của `/deduct`. Ở thời điểm đó khoản trừ chưa phải là cuối cùng.
</Warning>

### Tham số yêu cầu — `/v1/deduct`

| Trường        | Bắt buộc | Ghi chú                                                                                      |
| ------------- | -------- | -------------------------------------------------------------------------------------------- |
| `discordId`   | **Có**   | ID Discord của người dùng — một snowflake 17–20 chữ số                                       |
| `amount`      | **Có**   | Số nguyên dương                                                                              |
| `guildId`     | **Có**   | Máy chủ Discord nơi khoản trừ khởi phát. Mọi khoản trừ đều phải đến từ một máy chủ           |
| `reason`      | Không    | Tối đa 256 ký tự. Hiển thị cho người dùng và trả lại trong webhook                           |
| `merchantRef` | Không    | Tham chiếu của riêng bạn, tối đa 128 ký tự. Dùng để khớp webhook với hồ sơ của bạn           |
| `product`     | Không    | `{ type, name, description?, imageUrl? }` — hiện trên trang xác nhận và trong tin nhắn riêng |
| `imageUrl`    | Không    | Phải là `https://`                                                                           |
| `metadata`    | Không    | Tối đa **10** cặp khóa/giá trị dạng chuỗi, được chuyển tiếp nguyên vẹn                       |

## Cộng Vito cho người dùng

`POST /v1/add` cộng Vito cho một người dùng **từ số dư của chính bạn** — để thưởng hoặc hoàn tiền. Cùng các trường như `/deduct`, trừ `guildId` và `product`.

<Note>
  Khác với khoản trừ, khoản cộng **không có bước xác nhận** — nó được quyết toán ngay. Cần scope `credit:create` và đủ số dư, nếu không lệnh gọi trả về **402 `VITO_INSUFFICIENT_OWNER_FUNDS`**.
</Note>

## Phí

Mỗi khoản trừ được quyết toán cho bạn sau khi trừ phí nền tảng — cùng biểu phí với chuyển Vito trong ứng dụng, dựa trên cấp Hội viên của **bạn**:

| Hội viên của bạn     | Phí |
| -------------------- | --- |
| Normal, Silver, Gold | 7%  |
| Platinum             | 6%  |
| Diamond              | 5%  |

<Note>
  Các khoản **5 Vito trở xuống được miễn phí**, và khoản cộng qua `/v1/add` thì luôn miễn phí.
</Note>

## Webhook

Thêm một hoặc nhiều URL callback `https` ở tab cài đặt. Vito gửi một `POST` có chữ ký mỗi khi một xác nhận đạt tới trạng thái cuối.

<Warning>
  Webhook chỉ được gửi khi dự án của bạn có **cả** URL callback **và** khóa bí mật ký. Hãy hiện khóa bí mật (`whsec_…`) một lần duy nhất từ tab khóa API.
</Warning>

### Xác minh chữ ký

Mỗi lần gửi đều kèm header `X-Vito-Signature`:

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

Hai header khác đi kèm mỗi lần gửi — hãy dùng `X-Vito-Event-Id` làm khóa chống trùng lặp, vì lần thử lại sẽ gửi đúng id đó:

| Header              | Chứa                                                        |
| ------------------- | ----------------------------------------------------------- |
| `X-Vito-Event-Id`   | Id cố định cho sự kiện này — giống nhau qua mọi lần thử lại |
| `X-Vito-Event-Type` | Ví dụ `confirmation.completed`                              |

<Warning>
  **Trong lúc xoay vòng khóa bí mật ký, header mang nhiều hơn một chữ ký**, mới nhất trước:

  ```text theme={null}
  X-Vito-Signature: ts=<unix>;h1=<mới>;h1=<cũ>
  ```

  Hãy chấp nhận nếu **bất kỳ** `h1` nào khớp. Bộ xác minh chỉ đọc cái đầu tiên sẽ từ chối mọi webhook cho tới khi triển khai xong khóa mới — đúng thứ mà khoảng chồng lấn 24 giờ sinh ra để tránh.
</Warning>

Tính lại HMAC trên `<ts>:<rawBody>` bằng khóa bí mật ký của bạn và so sánh trong thời gian hằng số.

```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>
  Hãy xác minh trên **body thô**, trước khi việc phân tích JSON hay middleware viết lại nó.
</Warning>

### Sự kiện

Năm loại sự kiện, tất cả dùng chung một dạng payload. `data.status` mang kết quả.

| Sự kiện                  | Ý nghĩa                                                                                 |
| ------------------------ | --------------------------------------------------------------------------------------- |
| `confirmation.completed` | Đã duyệt và đã trừ. Kèm `transactionId`. **Hoàn tất hành động của bạn ở đây**           |
| `confirmation.failed`    | Không thể hoàn tất — thiếu số dư hoặc lỗi nội bộ. Kèm `failureReason`                   |
| `confirmation.expired`   | Không được xác nhận trong 10 phút. Không có Vito nào dịch chuyển                        |
| `confirmation.cancelled` | Người dùng đã hủy. Không có Vito nào dịch chuyển                                        |
| `credit.completed`       | Một `/add` do chủ sở hữu chi trả đã quyết toán. Kích hoạt ngay — không có bước xác nhận |

<Note>
  Webhook được thử lại **5 lần với thời gian chờ tăng dần**. Hãy phản hồi 2xx thật nhanh và xử lý giao hàng theo cách bất đồng bộ.
</Note>

## Mã lỗi

Mọi phản hồi đều được bọc trong một phong bì. Thành công mang `data`, thất bại mang `error`, không bao giờ có cả hai:

```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>
  **Mọi mã đều có tiền tố `VITO_`.** Hãy so khớp toàn bộ chuỗi — một `RATE_LIMITED` hay `FORBIDDEN` trần trụi không bao giờ xuất hiện trong phản hồi.
</Warning>

| Mã                                    | Trạng thái | Ý nghĩa                                                                            |
| ------------------------------------- | ---------- | ---------------------------------------------------------------------------------- |
| `VITO_VALIDATION_ERROR`               | 400        | Thiếu một trường hoặc trường sai định dạng                                         |
| `VITO_INVALID_AMOUNT`                 | 400        | `amount` không phải số nguyên dương                                                |
| `VITO_AMOUNT_LIMIT_EXCEEDED`          | 400        | Vượt trần mỗi giao dịch của bạn                                                    |
| `VITO_DAILY_VOLUME_EXCEEDED`          | 400        | Lệnh gọi này sẽ vượt trần khối lượng hằng ngày của bạn                             |
| `VITO_SELF_TRANSFER`                  | 400        | Người gửi và người nhận là cùng một người dùng                                     |
| `VITO_INVALID_API_KEY`                | 401        | Khóa thiếu, sai định dạng, đã thu hồi hoặc không xác định                          |
| `VITO_INSUFFICIENT_FUNDS`             | 402        | Người dùng không đủ số dư cho khoản trừ                                            |
| `VITO_INSUFFICIENT_OWNER_FUNDS`       | 402        | Số dư của **bạn** quá thấp để chi trả cho `/add`                                   |
| `VITO_INSUFFICIENT_SCOPE`             | 403        | Khóa thiếu scope mà endpoint yêu cầu                                               |
| `VITO_IP_NOT_ALLOWED`                 | 403        | IP gọi đến không nằm trong danh sách cho phép                                      |
| `VITO_OWNER_MEMBERSHIP_INACTIVE`      | 403        | Hội viên của chủ sở hữu đã hết hạn — đóng băng đến khi đăng ký lại                 |
| `VITO_GUILD_NOT_ALLOWED`              | 403        | `guildId` không nằm trong danh sách máy chủ cho phép của dự án                     |
| `VITO_TOS_NOT_ACCEPTED`               | 403        | Chưa chấp nhận điều khoản nhà phát triển, hoặc đang có phiên bản mới hơn chờ duyệt |
| `VITO_PROJECT_FROZEN`                 | 403        | Bị đóng băng — thường do Hội viên của chủ sở hữu hết hạn                           |
| `VITO_PROJECT_SUSPENDED`              | 403        | Bị đội ngũ Vetox đình chỉ                                                          |
| `VITO_PROJECT_BANNED`                 | 403        | Bị đội ngũ Vetox cấm                                                               |
| `VITO_USER_BLACKLISTED`               | 403        | Người dùng bị chặn khỏi các thao tác Vito                                          |
| `VITO_ACCOUNT_LOCKED`                 | 403        | Ví của người dùng bị khóa sau các lần nhập PIN sai                                 |
| `VITO_USER_NOT_FOUND`                 | 404        | Không có tài khoản Vito cho ID Discord đó                                          |
| `VITO_CONFIRMATION_NOT_FOUND`         | 404        | Token xác nhận không xác định                                                      |
| `VITO_IDEMPOTENCY_CONFLICT`           | 409        | Cùng `Idempotency-Key` nhưng khác body                                             |
| `VITO_IDEMPOTENCY_IN_PROGRESS`        | 409        | Một yêu cầu y hệt vẫn đang chạy — thử lại sau giây lát                             |
| `VITO_CONFIRMATION_ALREADY_PROCESSED` | 409        | Xác nhận đó đã đạt trạng thái cuối                                                 |
| `VITO_KEY_ROTATION_IN_PROGRESS`       | 409        | Đã có một lượt xoay vòng đang chạy                                                 |
| `VITO_CONFIRMATION_EXPIRED`           | 410        | Cửa sổ 10 phút đã trôi qua                                                         |
| `VITO_RATE_LIMITED`                   | 429        | Giảm nhịp rồi thử lại                                                              |
| `VITO_INTERNAL_ERROR`                 | 500        | Sự cố ngoài dự kiến ở phía chúng tôi                                               |

## Giới hạn tần suất

| Hội viên của chủ sở hữu | Mỗi phút | Mỗi giờ |
| ----------------------- | -------- | ------- |
| Không có                | 60       | 1.000   |
| Silver hoặc Gold        | 180      | 5.000   |
| Platinum hoặc Diamond   | 600      | 15.000  |

Còn có giới hạn theo từng IP bằng một nửa hạn mức mỗi phút của bạn, với mức sàn là 30.

<Warning>
  Khi tải cao, **các endpoint ghi thất bại theo hướng đóng** — khoản trừ bị từ chối thay vì mạo hiểm chi tiêu trùng. Các endpoint đọc thất bại theo hướng mở. Hãy coi một thao tác ghi bị từ chối là "chưa xảy ra" và thử lại.
</Warning>

<Note>
  Gửi header `Idempotency-Key` để chống trùng lặp khi thử lại một cách an toàn.
</Note>

## Endpoint

| Endpoint                         | Scope               |
| -------------------------------- | ------------------- |
| `GET /v1/auth/verify`            | bất kỳ              |
| `POST /v1/auth/rotate-key`       | bất kỳ              |
| `GET /v1/balance/:discordId`     | `balance:read`      |
| `POST /v1/deduct`                | `deduct:create`     |
| `POST /v1/add`                   | `credit:create`     |
| `POST /v1/transfer`              | `transfer:create`   |
| `GET /v1/transactions` và `/:id` | `transactions:read` |
| `GET /v1/webhooks/events`        | `transactions:read` |

## Hạn mức

* Xác nhận hết hạn sau **10 phút** — hãy coi các yêu cầu chưa xác nhận là đã bỏ dở
* `amount` phải là số nguyên dương
* `metadata` giới hạn ở 10 khóa
* Trần mỗi giao dịch và trần hằng ngày do đội ngũ Vetox đặt và chỉ hiển thị ở chế độ đọc trong tab cài đặt

## Danh sách kiểm tra bảo mật

<AccordionGroup>
  <Accordion title="Giữ khóa bí mật ở phía máy chủ" icon="lock">
    Khóa API và khóa bí mật ký không bao giờ thuộc về mã phía client. Hãy xoay vòng ngay nếu một trong hai bị lộ.
  </Accordion>

  <Accordion title="Xác minh mọi webhook" icon="signature">
    Kiểm tra chữ ký trên body thô và từ chối những lần gửi cũ hơn \~5 phút.
  </Accordion>

  <Accordion title="Chỉ quyết toán khi completed" icon="circle-check">
    Đừng bao giờ giao hàng dựa trên phản hồi `/deduct` — khoản trừ chỉ là cuối cùng khi có `confirmation.completed`.
  </Accordion>

  <Accordion title="Đặc quyền tối thiểu" icon="key">
    Chỉ xin những scope bạn thực sự dùng, và bật danh sách IP cho phép.
  </Accordion>
</AccordionGroup>

## Khắc phục sự cố

<AccordionGroup>
  <Accordion title="Mọi lệnh gọi đều trả về không được phép">
    Hội viên của chủ sở hữu đã hết hạn. Nó được kiểm tra lại ở mọi lệnh gọi.
  </Accordion>

  <Accordion title="Tôi làm mất khóa">
    Không thể khôi phục — chỉ một bản băm được lưu. Hãy xoay vòng để lấy khóa mới.
  </Accordion>

  <Accordion title="Chữ ký webhook lỗi sau khi xoay vòng">
    Hãy chấp nhận cả hai khóa bí mật trong khoảng chồng lấn 24 giờ.
  </Accordion>

  <Accordion title="Một khoản trừ không bao giờ hoàn tất">
    Người dùng đã không phê duyệt. Xác nhận hết hạn sau 10 phút.
  </Accordion>

  <Accordion title="Không có webhook nào đến">
    Một dự án cần cả URL callback lẫn khóa bí mật ký. Chỉ có một trong hai thì không có gì được gửi đi.
  </Accordion>

  <Accordion title="Vito nhận về ít hơn số tôi đã trừ">
    Đó là phí quyết toán. Dùng các khoản 5 Vito trở xuống để tránh nó, hoặc tính nó vào giá của bạn.
  </Accordion>

  <Accordion title="402 VITO_INSUFFICIENT_OWNER_FUNDS">
    `/v1/add` được chi trả từ số dư của chính bạn, không phải tạo ra từ hư không. Hãy nạp thêm.
  </Accordion>
</AccordionGroup>

<CardGroup cols={2}>
  <Card title="Vito" icon="coins" href="/vi/members/vito">
    Số dư, mã PIN và phí.
  </Card>

  <Card title="Yêu cầu thanh toán" icon="receipt" href="/vi/account/payment-requests">
    Những gì người dùng thấy khi bạn trừ tiền họ.
  </Card>
</CardGroup>
