개요
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_xxxxxxxxxxxxxxxx두 가지 선택적 계층이 프로젝트를 추가로 보호합니다:
- 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를 차감하고 거래를 기록하며 — 웹훅을 구성했다면 — 서명된 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
}웹훅 및 서명 검증
설정 탭에서 하나 이상의 https 콜백 URL을 추가하세요. Vito는 확인이 최종 상태에 도달할 때마다 구성된 각 URL로 서명된 POST를 전달하며, 지수 백오프로 최대 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
});웹훅 이벤트 및 페이로드
Vito는 네 가지 이벤트 유형 중 하나를 전송합니다. 모두 동일한 페이로드 형식을 공유하며 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 → 웹훅 → 완료 과정을 거칩니다.
- 1데모 통합 폴더를 열고 Vito API 키, Discord 봇 토큰, 테스트 서버(guild) ID를 config.json에 추가하세요.
- 2npm install && npm start를 실행한 다음 http://localhost:4000을 여세요.
- 3Discord ID로 사용자를 검색하고 항목을 선택한 뒤 청구를 시작하세요 — 데모가 /deduct를 호출하고 confirmUrl을 표시합니다.
- 4vetox.io에서 PIN을 승인하면 데모가 완료를 감지하고(구성된 경우 웹훅, 아니면 폴링) 샘플 작업을 완료합니다.
모범 사례, 제한 및 보안
보안
- API 키와 서명 시크릿을 서버 측에만 보관하고, 둘 중 하나라도 유출되면 즉시 교체하세요.
- 웹훅 서명을 항상 원시 본문에 대해 검증하고 약 5분보다 오래된 전달은 거부하세요(재생 방지).
- 자체 작업은 confirmation.completed에서만 완료하세요 — /deduct 응답에서는 절대 하지 마세요(그 시점에 청구는 확정되지 않았습니다).
- IP 허용 목록을 사용하고 실제로 필요한 스코프만 요청하세요.
제한
- 확인은 10분 후 만료됩니다. 확인되지 않은 요청은 포기된 것으로 처리하세요.
- 속도 제한은 프로젝트별이며 소유자의 멤버십 등급에 따라 확장됩니다.
- amount는 양의 정수여야 하며 metadata는 최대 10개 키로 제한됩니다.