> ## 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 と購入

> 承認済みアプリに PIN 確認付きで Vito を課金させ、購入ページから決済リクエストを管理します。

<Info>
  **Silver** 以上のメンバーシップが必要です — 申請時だけでなく、認証済みのすべての呼び出しでも同様です。キー所有者のメンバーシップが失効すると、再加入するまでプロジェクトは凍結されます。
</Info>

アプリケーションが Discord の中からユーザーの [Vito](/ja/members/vito) 残高を扱うための REST API です。残高の読み取り、請求、付与、ユーザー間の移動ができます。すべてのエンドポイントは JSON を返し、`/v1` 配下でバージョン管理されています。

<Warning>
  **Vito が実際のお金と行き来することは決してありません。** Vetox の残高どうしの間だけを移動します。
</Warning>

## アクセスの取得

アクセスは**プロジェクト単位**で付与されます。次の 4 つすべてが必要です。

<Steps>
  <Step title="有効な Silver 以上のメンバーシップ">
    承認時だけでなく、呼び出しのたびに確認されます。
  </Step>

  <Step title="承認された開発者申請">
    ダッシュボードの Vito API ページから送信します。Vetox チームが手作業で審査します。
  </Step>

  <Step title="API 開発者規約への同意">
    申請を送信する際に承諾します。
  </Step>

  <Step title="プロジェクトに必要なスコープ">
    記載内容に基づいて Vetox チームが付与します。
  </Step>
</Steps>

<Tip>
  何を作るのか、キーをどう保管するのかを具体的に書いてください。却下されるのは決まって曖昧な申請です。
</Tip>

### スコープ

| スコープ                | 許可されること                   |
| ------------------- | ------------------------- |
| `balance:read`      | ユーザーの Vito 残高を読み取る        |
| `deduct:create`     | ユーザーの残高から請求する             |
| `credit:create`     | 自分の残高を原資にユーザーへ Vito を付与する |
| `transfer:create`   | 2 人のユーザー間で Vito を移動する     |
| `transactions:read` | プロジェクトの取引を一覧・参照する         |

## 認証

シークレットキーを Bearer トークンとして送信します。

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

任意で追加できる 2 つの層が、プロジェクトをさらに堅牢にします。

* **IP 許可リスト** — 呼び出しを特定のサーバー IP に限定します
* **レート制限** — 所有者のメンバーシップ階層に応じて上がる、プロジェクトごとの上限です

### キーのローテーションと保管

<Warning>
  **キーと署名シークレットが表示されるのは一度だけです。** 承認後、API キータブでそれらを表示するための**7 日間の猶予**があります。Vetox はハッシュしか保持しておらず再表示はできません。期間を過ぎた場合はローテーションして取得し直してください。
</Warning>

* **サーバー側だけ**に保管してください — 手にした者は誰でもあなたのユーザーに請求できます
* ローテーションは API キータブから行います。以前のキーは **24 時間の猶予期間**にわたり有効なままなので、停止なしでデプロイできます
* webhook の署名シークレットは別枠でローテーションされ、独自の 24 時間の重複期間を持ちます
* 漏洩した場合は直ちにローテーションしてください

## ユーザーへの請求

<Warning>
  **キーだけでユーザーの Vito を動かすことはできません。** すべての請求には、`vetox.io` 上でユーザーがウォレットの PIN によって承認する必要があります。あなたのアプリ内でも Discord 内でも行われません。
</Warning>

<Steps>
  <Step title="アプリが POST /v1/deduct を呼び出す">
    ユーザー、金額、発生元の `guildId`、商品の詳細を添えて送信します。
  </Step>

  <Step title="Vito が confirmUrl を返す">
    保留中の確認で、有効期間は **10 分**です。ユーザーには DM も届きます。
  </Step>

  <Step title="ユーザーが PIN で承認する">
    `vetox.io` 上で行います。
  </Step>

  <Step title="Vito が決済して通知する">
    残高が引き落とされ、取引が記録され、設定していれば署名付き webhook が送信されます。
  </Step>

  <Step title="アプリが検証して完了する">
    署名を確認してから、コンテンツを解放するか商品を引き渡します。
  </Step>
</Steps>

<Warning>
  **処理を完了させるのは `confirmation.completed` を受け取ったときだけ**にしてください。`/deduct` のレスポンスで完了させてはいけません。その時点では請求はまだ確定していません。
</Warning>

### リクエストパラメーター — `/v1/deduct`

| フィールド         | 必須     | 備考                                                            |
| ------------- | ------ | ------------------------------------------------------------- |
| `discordId`   | **はい** | ユーザーの Discord ID — 17〜20 桁の snowflake                         |
| `amount`      | **はい** | 正の整数                                                          |
| `guildId`     | **はい** | 請求の発生元となる Discord サーバー。すべての請求はサーバー起点である必要があります                |
| `reason`      | いいえ    | 最大 256 文字。ユーザーに表示され、webhook にも含まれます                           |
| `merchantRef` | いいえ    | 自社の参照値、最大 128 文字。webhook と自社の記録を突き合わせるのに使います                  |
| `product`     | いいえ    | `{ type, name, description?, imageUrl? }` — 確認ページと DM に表示されます |
| `imageUrl`    | いいえ    | `https://` である必要があります                                         |
| `metadata`    | いいえ    | 文字列のキー／値ペアを最大 **10** 組。そのまま引き渡されます                            |

## ユーザーへの付与

`POST /v1/add` は**自分の残高を原資に**ユーザーへ Vito を付与します。報酬や返金に使います。フィールドは `guildId` と `product` を除いて `/deduct` と同じです。

<Note>
  請求とは異なり、付与に**確認ステップはありません** — 即時に決済されます。`credit:create` スコープと十分な残高が必要で、足りない場合は **402 `VITO_INSUFFICIENT_OWNER_FUNDS`** が返ります。
</Note>

## 手数料

すべての請求は、プラットフォーム手数料を差し引いてあなたに支払われます。アプリ内の Vito 送金と同じ料率で、**あなたの**メンバーシップ階層に基づきます。

| あなたのメンバーシップ        | 手数料 |
| ------------------ | --- |
| Normal、Silver、Gold | 7%  |
| Platinum           | 6%  |
| Diamond            | 5%  |

<Note>
  **5 Vito 以下の金額は手数料無料**で、`/v1/add` による付与は常に手数料無料です。
</Note>

## Webhook

設定タブで `https` のコールバック URL を 1 つ以上追加します。確認が終了状態に達するたびに、Vito が署名付きの `POST` を送信します。

<Warning>
  webhook が送信されるのは、プロジェクトにコールバック URL **と**署名シークレットの**両方**がある場合だけです。シークレット（`whsec_…`）は API キータブから一度だけ表示できます。
</Warning>

### 署名の検証

すべての配信には `X-Vito-Signature` ヘッダーが付きます。

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

さらに 2 つのヘッダーが各配信に付随します。再送では同じ id が送られるため、`X-Vito-Event-Id` を重複排除キーとして使ってください。

| ヘッダー                | 内容                          |
| ------------------- | --------------------------- |
| `X-Vito-Event-Id`   | このイベントの固定 id — 再送しても変わりません  |
| `X-Vito-Event-Type` | 例: `confirmation.completed` |

<Warning>
  \*\*署名シークレットのローテーション中、ヘッダーは複数の署名を含みます。\*\*新しいものが先です。

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

  `h1` の**いずれか**が一致すれば配信を受け入れてください。最初の 1 つしか読まない検証コードは、新しいシークレットをデプロイし終えるまですべての webhook を拒否します。これでは 24 時間の重複期間を設けた意味がなくなります。
</Warning>

署名シークレットで `<ts>:<rawBody>` の HMAC を再計算し、一定時間で比較してください。

```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>
  JSON パースやミドルウェアが書き換える前の、**生のボディ**に対して検証してください。
</Warning>

### イベント

イベントは 5 種類で、すべて同じペイロード形式を共有します。結果は `data.status` が保持します。

| イベント                     | 意味                                                      |
| ------------------------ | ------------------------------------------------------- |
| `confirmation.completed` | 承認され引き落とされました。`transactionId` を含みます。**処理はここで完了させてください** |
| `confirmation.failed`    | 完了できませんでした — 残高不足または内部エラー。`failureReason` を含みます         |
| `confirmation.expired`   | 10 分以内に確認されませんでした。Vito は移動していません                        |
| `confirmation.cancelled` | ユーザーがキャンセルしました。Vito は移動していません                           |
| `credit.completed`       | 所有者が原資となる `/add` が決済されました。確認ステップなしで即時に発火します             |

<Note>
  webhook はバックオフを挟んで **5 回**再試行されます。速やかに 2xx を返し、引き渡し処理は非同期で行ってください。
</Note>

## エラーコード

すべてのレスポンスはエンベロープに包まれます。成功時は `data`、失敗時は `error` を持ち、両方が同時に入ることはありません。

```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>
  \*\*すべてのコードには `VITO_` が前置されます。\*\*完全な文字列で判定してください。`RATE_LIMITED` や `FORBIDDEN` が単独で返ることはありません。
</Warning>

| コード                                   | ステータス | 意味                               |
| ------------------------------------- | ----- | -------------------------------- |
| `VITO_VALIDATION_ERROR`               | 400   | フィールドが欠けているか形式が不正です              |
| `VITO_INVALID_AMOUNT`                 | 400   | `amount` が正の整数ではありません            |
| `VITO_AMOUNT_LIMIT_EXCEEDED`          | 400   | 1 取引あたりの上限を超えています                |
| `VITO_DAILY_VOLUME_EXCEEDED`          | 400   | この呼び出しは 1 日の取扱高の上限を超えます          |
| `VITO_SELF_TRANSFER`                  | 400   | 送金元と送金先が同一ユーザーです                 |
| `VITO_INVALID_API_KEY`                | 401   | キーが未指定・不正・失効済み・不明です              |
| `VITO_INSUFFICIENT_FUNDS`             | 402   | ユーザーの残高が請求額に足りません                |
| `VITO_INSUFFICIENT_OWNER_FUNDS`       | 402   | `/add` の原資となる**あなたの**残高が不足しています  |
| `VITO_INSUFFICIENT_SCOPE`             | 403   | このエンドポイントに必要なスコープがキーにありません       |
| `VITO_IP_NOT_ALLOWED`                 | 403   | 呼び出し元 IP が許可リストにありません            |
| `VITO_OWNER_MEMBERSHIP_INACTIVE`      | 403   | 所有者のメンバーシップが失効しました — 再加入まで凍結されます |
| `VITO_GUILD_NOT_ALLOWED`              | 403   | `guildId` がプロジェクトの許可サーバー一覧にありません |
| `VITO_TOS_NOT_ACCEPTED`               | 403   | 開発者規約が未同意、または新しい版が保留中です          |
| `VITO_PROJECT_FROZEN`                 | 403   | 凍結中 — 多くは所有者のメンバーシップ失効によるものです    |
| `VITO_PROJECT_SUSPENDED`              | 403   | Vetox チームにより停止されました              |
| `VITO_PROJECT_BANNED`                 | 403   | Vetox チームにより禁止されました              |
| `VITO_USER_BLACKLISTED`               | 403   | このユーザーは Vito の操作を禁止されています        |
| `VITO_ACCOUNT_LOCKED`                 | 403   | PIN の入力失敗によりユーザーのウォレットがロックされています |
| `VITO_USER_NOT_FOUND`                 | 404   | その Discord ID の Vito アカウントがありません |
| `VITO_CONFIRMATION_NOT_FOUND`         | 404   | 確認トークンが不明です                      |
| `VITO_IDEMPOTENCY_CONFLICT`           | 409   | 同じ `Idempotency-Key` で本文が異なります   |
| `VITO_IDEMPOTENCY_IN_PROGRESS`        | 409   | 同一のリクエストが処理中です — 少し後に再試行してください   |
| `VITO_CONFIRMATION_ALREADY_PROCESSED` | 409   | その確認はすでに終了状態に達しています              |
| `VITO_KEY_ROTATION_IN_PROGRESS`       | 409   | ローテーションがすでに進行中です                 |
| `VITO_CONFIRMATION_EXPIRED`           | 410   | 10 分の有効期間が過ぎました                  |
| `VITO_RATE_LIMITED`                   | 429   | 間隔を空けて再試行してください                  |
| `VITO_INTERNAL_ERROR`                 | 500   | 当方側の予期せぬ障害です                     |

## レート制限

| 所有者のメンバーシップ          | 1 分あたり | 1 時間あたり |
| -------------------- | ------ | ------- |
| なし                   | 60     | 1,000   |
| Silver または Gold      | 180    | 5,000   |
| Platinum または Diamond | 600    | 15,000  |

さらに、1 分あたりの上限の半分（下限 30）が IP ごとの制限として適用されます。

<Warning>
  高負荷時、**書き込みエンドポイントはフェイルクローズします** — 二重支払いのリスクを取るより請求を拒否します。読み取りはフェイルオープンします。拒否された書き込みは「起きなかった」ものとして扱い、再試行してください。
</Warning>

<Note>
  再試行を安全に重複排除するには `Idempotency-Key` ヘッダーを送信してください。
</Note>

## エンドポイント

| エンドポイント                         | スコープ                |
| ------------------------------- | ------------------- |
| `GET /v1/auth/verify`           | 任意                  |
| `POST /v1/auth/rotate-key`      | 任意                  |
| `GET /v1/balance/:discordId`    | `balance:read`      |
| `POST /v1/deduct`               | `deduct:create`     |
| `POST /v1/add`                  | `credit:create`     |
| `POST /v1/transfer`             | `transfer:create`   |
| `GET /v1/transactions` と `/:id` | `transactions:read` |
| `GET /v1/webhooks/events`       | `transactions:read` |

## 制限

* 確認は **10 分**で失効します — 未確認のリクエストは破棄されたものとして扱ってください
* `amount` は正の整数である必要があります
* `metadata` は 10 キーまでです
* 1 取引あたりと 1 日あたりの上限は Vetox チームが設定し、設定タブに読み取り専用で表示されます

## セキュリティチェックリスト

<AccordionGroup>
  <Accordion title="シークレットはサーバー側に置く" icon="lock">
    API キーと署名シークレットをクライアントコードに置いてはいけません。いずれかが漏洩したら直ちにローテーションしてください。
  </Accordion>

  <Accordion title="すべての webhook を検証する" icon="signature">
    生のボディに対して署名を確認し、約 5 分より古い配信は拒否してください。
  </Accordion>

  <Accordion title="completed のときだけ決済する" icon="circle-check">
    `/deduct` のレスポンスで引き渡してはいけません。請求は `confirmation.completed` で初めて確定します。
  </Accordion>

  <Accordion title="最小権限" icon="key">
    実際に使うスコープだけを申請し、IP 許可リストを有効にしてください。
  </Accordion>
</AccordionGroup>

## トラブルシューティング

<AccordionGroup>
  <Accordion title="すべての呼び出しが未認証を返す">
    所有者のメンバーシップが失効しています。呼び出しのたびに再確認されます。
  </Accordion>

  <Accordion title="キーを紛失した">
    復元はできません — 保存されているのはハッシュだけです。ローテーションして新しいものを取得してください。
  </Accordion>

  <Accordion title="ローテーション後に webhook の署名検証が失敗する">
    24 時間の重複期間中は両方のシークレットを受け入れてください。
  </Accordion>

  <Accordion title="請求がいつまでも完了しない">
    ユーザーが承認していません。確認は 10 分で失効します。
  </Accordion>

  <Accordion title="webhook がまったく届かない">
    プロジェクトにはコールバック URL と署名シークレットの両方が必要です。片方だけでは何も配信されません。
  </Accordion>

  <Accordion title="請求した額より少ない Vito しか届かない">
    決済手数料です。避けるには 5 Vito 以下の金額を使うか、価格に織り込んでください。
  </Accordion>

  <Accordion title="402 VITO_INSUFFICIENT_OWNER_FUNDS">
    `/v1/add` はあなた自身の残高を原資とし、無から作られるものではありません。残高を補充してください。
  </Accordion>
</AccordionGroup>

<CardGroup cols={2}>
  <Card title="Vito" icon="coins" href="/ja/members/vito">
    残高、PIN、手数料について。
  </Card>

  <Card title="支払いリクエスト" icon="receipt" href="/ja/account/payment-requests">
    請求したときにユーザーに見えるもの。
  </Card>
</CardGroup>
