概要
Vito APIを使うと、アプリはDiscord内からユーザーのVito残高を扱えます — 残高の読み取り、課金、または払い戻し(クレジット)です。ユーザーはVitoが移動する前に、vetox.ioでPINを使って各課金を確認し、結果がアプリに通知されます。Vitoは Vetox のエコシステム内仮想通貨です。APIを使って Vito ベースの体験を構築できます — コンテンツ・機能・特典・報酬のアンロックなど。実際のお金による売買は一切行われません。
これは秘密キーで認証するREST APIです。すべてのエンドポイントはJSONを返し、/v1の下でバージョン管理されています。
https://api.vetox.io/public/vito/v1残高と精算
APIキーはあなたのVetoxアカウントに紐づいています。すべての課金はあなたに精算されます。アプリがユーザーからVitoを差し引くと、そのVitoは(プラットフォーム手数料を差し引いて)あなた自身の残高に入金され、消滅することはありません。アプリがユーザーにVitoを追加する場合は、あなたの残高から支払われます。
Vitoは常に残高間で移動し、実際のお金との間で出入りすることはありません。ユーザーにはあなたが請求した全額が課金され、あなたはプラットフォーム手数料を差し引いた額を受け取ります。クレジットや報酬は /add を通じてあなたの残高から賄われます。
要件
Vito APIへのアクセスはプロジェクトごとに付与されます。呼び出す前に必要なもの:
- 有効なユーザーメンバーシップ(Silver以上)。所有者のメンバーシップが失効するとAPIは自動的に凍結され、再登録すると復旧します。
- 承認された開発者申請。このページから申請してください。Vetoxチームが手動で審査します。
- API 開発者規約への同意 — 開発者申請を送信した時点で承認されます。
- プロジェクトに必要なスコープ。申請内容に基づきVetoxチームが付与します。
利用可能なスコープ
balance:readdeduct:createcredit:createtransfer:createtransactions:read認証
すべてのリクエストを、Authorizationヘッダーに秘密APIキーをBearerトークンとして付けて認証します。キーには vito_live_ のプレフィックスが付きます。
Authorization: Bearer vito_live_xxxxxxxxxxxxxxxx2つの任意のレイヤーがプロジェクトをさらに保護します:
- IP許可リスト — リクエストを特定のサーバーIPに制限します(設定タブで構成)。
- レート制限 — メンバーシップに応じて拡張されるプロジェクトごとのリクエスト上限。
APIキー — 保管、ローテーション、セキュリティ
キーは申請が承認されたときに生成されます。一度だけ表示されるので、APIキータブから表示してすぐに保管してください。
Vetoxはキーを読み取り可能な形では決して保存しません(ソルト付きハッシュのみ)。環境変数やシークレットマネージャーなどサーバー側のシークレットとして保管し、クライアントコード・gitリポジトリ・ブラウザには絶対に置かないでください。
APIキータブからローテーションします。古いキーは新しいキーを無停止で展開できるよう24時間の猶予期間中は動作し続け、その後停止します。
課金確認フロー
すべての課金は、ユーザーがPINで承認する確認を経ます — キーだけでは決してVitoは動きません:
- 1アプリはユーザー・金額・発生元のguildId・アイテム詳細を付けてPOST /deductを呼び出します。
- 2Vitoは保留中の確認(10分間有効)を作成し、confirmUrlを返します。guildIdが指定されると、ユーザーには詳細と確認ページへのボタンを含むDMも送信されます。
- 3ユーザーはvetox.ioでconfirmUrlを開き、ウォレットのPINで課金を承認します。
- 4VitoはVitoを差し引き、取引を記録し — Webhookを構成していれば — 署名付きのconfirmation.completedイベントをコールバックURLに送信します。
- 5アプリは署名を検証し、自分のアクションを完了します(例: コンテンツをアンロック)。
リクエストパラメータ(/deduct)
/v1/deduct差し引きエンドポイントは次のボディフィールドを受け付けます:
discordIdamountguildIdreasonmerchantRefproductproduct.imageUrlmetadata* 必須フィールド。
POST https://api.vetox.io/public/vito/v1/deduct
Authorization: Bearer vito_live_xxxxxxxxxxxxxxxx
Content-Type: application/json
Idempotency-Key: req_9a1f2b # optional, dedupes retries for 24h
{
"discordId": "123456789012345678",
"amount": 500,
"guildId": "1470389021162209280",
"reason": "Unlock premium theme",
"merchantRef": "ref_1042",
"product": {
"type": "feature",
"name": "Premium theme",
"description": "Unlocks the premium theme pack",
"imageUrl": "https://cdn.example.com/theme.png"
},
"metadata": { "ref": "1042" }
}{
"success": true,
"data": {
"confirmationId": "f3c9...e1",
"confirmUrl": "https://vetox.io/confirm/vito/f3c9...e1",
"amount": 500,
"expiresAt": 1735689600000,
"status": "pending"
},
"requestId": "req_abc123",
"timestamp": 1735689300000
}Vitoの追加 — クレジット(/add)
/v1/addPOST /add は、あなた自身の残高からユーザーにVitoを入金します(報酬やクレジット用)。guildId と product を除き /deduct と同じフィールドを受け取ります。課金と異なり即時で、ユーザーのPINは不要です(APIキーがあなたの権限です)。そのため応答にはすでに完了した送金が反映されます。ユーザーは全額を受け取り、プラットフォーム手数料はかかりません。
POST https://api.vetox.io/public/vito/v1/add
Authorization: Bearer vito_live_xxxxxxxxxxxxxxxx
Content-Type: application/json
Idempotency-Key: credit_7f3a # optional, dedupes retries for 24h
{
"discordId": "123456789012345678",
"amount": 250,
"reason": "Event reward",
"merchantRef": "reward_1042",
"metadata": { "ref": "1042" }
}{
"success": true,
"data": {
"transactionId": "txn_91b2",
"toDiscordId": "123456789012345678",
"amount": 250,
"ownerBalance": 48250,
"recipientBalance": 1250,
"status": "completed"
},
"requestId": "req_def456",
"timestamp": 1735689300000
}Webhookと署名検証
設定タブで1つ以上のhttpsコールバックURLを追加します。Vitoは確認が最終状態に達するたびに、署名付きPOSTを各構成済みURLに配信し、指数バックオフで最大5回まで再試行します。
署名の検証
各配信には ts=<unix>;h1=<hex> 形式のX-Vito-Signatureヘッダーが付きます。署名シークレットで "<ts>:<rawBody>" に対してHMACを再計算し、一定時間で比較してください。JSONを解析する前に、必ず生のリクエストボディに対して検証してください。
signature = HMAC_SHA256(signingSecret, "<ts>:<rawBody>")
header = X-Vito-Signature: ts=<unix-seconds>;h1=<hex-signature>import crypto from 'crypto';
import express from 'express';
const app = express();
// IMPORTANT: verify against the RAW body, before JSON parsing.
app.post('/webhooks/vito', express.raw({ type: '*/*' }), (req, res) => {
const sig = req.header('X-Vito-Signature') || ''; // "ts=<unix>;h1=<hmac>[;h1=<hmac>]"
const pairs = sig.split(';').map(p => p.split('='));
const ts = pairs.find(([k]) => k === 'ts')?.[1];
const sigs = pairs.filter(([k]) => k === 'h1').map(([, v]) => v);
const rawBody = req.body.toString('utf8');
const expected = crypto
.createHmac('sha256', process.env.VITO_SIGNING_SECRET) // whsec_...
.update(`${ts}:${rawBody}`)
.digest('hex');
const expectedBuf = Buffer.from(expected, 'hex');
// During a 24h secret rotation the header carries TWO h1= values — accept ANY.
// Length-guard FIRST: crypto.timingSafeEqual throws on a length mismatch.
const ok = sigs.some(h => {
const got = Buffer.from(h, 'hex');
return (
got.length === expectedBuf.length &&
crypto.timingSafeEqual(got, expectedBuf)
);
});
// Reject anything older than ~5 minutes to stop replays.
const fresh = ts && Math.abs(Date.now() / 1000 - Number(ts)) < 300;
if (!ok || !fresh) return res.status(401).end();
const event = JSON.parse(rawBody);
if (event.eventType === 'confirmation.completed') {
grantPremiumTheme(event.data.merchantRef, event.data.fromDiscordId);
}
res.status(200).end(); // ack fast; run your action async if slow
});Webhookイベントとペイロード
Vitoは4種類のイベントのいずれかを送信します。すべて同じペイロード形式を共有し、data.statusが結果を反映します。
confirmation.completedconfirmation.failedconfirmation.expiredconfirmation.cancelledcredit.completedPOST https://your-app.com/webhooks/vito
X-Vito-Signature: ts=1735689600;h1=4f8b2c...e9
X-Vito-Event-Id: evt_3a2b1c
X-Vito-Event-Type: confirmation.completed
Content-Type: application/json
{
"eventId": "evt_3a2b1c",
"eventType": "confirmation.completed",
"timestamp": 1735689600000,
"data": {
"confirmationId": "f3c9...e1",
"type": "deduct",
"fromDiscordId": "123456789012345678",
"toDiscordId": null,
"amount": 500,
"reason": "Unlock premium theme",
"merchantRef": "ref_1042",
"product": { "type": "feature", "name": "Premium theme" },
"status": "completed",
"transactionId": "txn_77a2",
"metadata": { "ref": "1042" },
"failureReason": null
}
}エラーコード
エラーは、トップレベルのerror.codeを含むJSONボディと共に2xx以外のステータスを返します。よくあるケース:
VITO_VALIDATION_ERRORinvalid keyOWNER_MEMBERSHIP_INACTIVEscope / IPnot foundinsufficient balanceINSUFFICIENT_OWNER_FUNDSrate limited{
"success": false,
"error": { "type": "Bad Request", "message": "guildId must be a valid Discord server ID", "code": "VITO_VALIDATION_ERROR" },
"requestId": "req_abc123",
"timestamp": 1735689300000
}統合の例
課金リクエストを作成:
/v1/deductcurl -X POST https://api.vetox.io/public/vito/v1/deduct \
-H "Authorization: Bearer $VITO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"discordId": "123456789012345678",
"amount": 500,
"guildId": "1470389021162209280",
"merchantRef": "ref_1042",
"product": { "type": "feature", "name": "Premium theme" }
}'ユーザーの残高を確認:
/v1/balance/:discordIdcurl https://api.vetox.io/public/vito/v1/balance/123456789012345678 \
-H "Authorization: Bearer $VITO_API_KEY"デモ統合でのテスト
コードを書かずに全フローをエンドツーエンドで試せるスタンドアロンのデモ統合が用意されています。リクエスト→確認→PIN→Webhook→完了を一通り体験できます。
- 1デモ統合フォルダーを開き、Vito APIキー、Discordボットトークン、テストサーバー(guild)IDをconfig.jsonに追加します。
- 2npm install && npm start を実行し、http://localhost:4000 を開きます。
- 3Discord IDでユーザーを検索し、アイテムを選んで課金を開始します — デモは/deductを呼び出してconfirmUrlを表示します。
- 4vetox.ioでPINを承認すると、デモは完了を検出し(構成済みならWebhook、なければポーリング)、サンプルのアクションを完了します。
ベストプラクティス、制限、セキュリティ
セキュリティ
- APIキーと署名シークレットはサーバー側のみに保持し、いずれかが漏洩したらすぐにローテーションしてください。
- Webhook署名は常に生のボディに対して検証し、約5分より古い配信は拒否してください(リプレイ保護)。
- 自分のアクションの完了はconfirmation.completedのときだけにしてください — /deductのレスポンスでは決して行わないでください(その時点で課金は確定していません)。
- IP許可リストを使用し、本当に必要なスコープだけを要求してください。
制限
- 確認は10分後に期限切れになります。未確認のリクエストは放棄されたものとして扱ってください。
- レート制限はプロジェクトごとで、所有者のメンバーシップに応じて拡張されます。
- amountは正の整数である必要があります。metadataは最大10キーまでです。