S-19 · v1
Checkout API
Send the member to the VBank checkout window and get the result by webhook or the lookup API. Signing, sign-in and payment all happen on www.bankofv.co.kr. Your page may only open it by redirect or popup; iframes are blocked.
Getting started
- Ask a VBank operator to register your allowed origins (https), webhook URL (https, port 443, public IP) and the collections you sell.
- The operator issues API keys (vb_test_… = Amoy test, vb_live_… = Polygon live) and one webhook secret per mode (whsec_…). The plain text is shown once, when it is issued.
- Keep keys on your server only. Never put them in a browser or an app.
Test keys work only on the test environment, live keys only on https://www.bankofv.co.kr. The other way round returns 403 MODE_NOT_SERVED.
Basics
- Base URL: https://www.bankofv.co.kr/api/v1
- Auth: Authorization: Bearer vb_live_…
- Requests and responses are JSON. Responses carry Cache-Control: no-store.
- Amounts are integer strings in USDT minor units (6 decimals). "125000000" = 125 USDT.
- Addresses and hashes are lowercase 0x…; times are UTC ISO 8601 to the second.
- Rate limit: 120 per minute per key, 600 per minute per IP. Beyond that: 429 RATE_LIMITED.
- Errors: {"error": "<code>", "message": "<text>"}
Error codes
- 400 INVALID_REQUEST
- Malformed body (message names the field)
- 400 INVALID_JSON / INVALID_IDEMPOTENCY_KEY
- Not JSON / bad key format
- 400 INVALID_URL / URL_NOT_ALLOWED
- success/cancel URL is not https or its origin is not registered
- 401 INVALID_API_KEY
- Missing, wrong or revoked key
- 403 MODE_NOT_SERVED
- Key of a mode this environment does not serve
- 403 PARTNER_SUSPENDED
- Partner suspended. Only new sessions are blocked
- 403 COLLECTION_NOT_ALLOWED
- Item outside your allowed collections
- 404 ITEM_NOT_FOUND / SESSION_NOT_FOUND
- No such item or session
- 409 ITEM_UNAVAILABLE / SOLD_OUT
- Sale not open or ended, order expired / not enough left
- 409 IDEMPOTENCY_KEY_REUSED
- Same Idempotency-Key used with a different body
- 409 SESSION_PROCESSING / SESSION_FINAL
- Payment in progress or already finished, so it cannot be cancelled
- 413 TOO_LARGE
- Body over 16 KB
- 422 QUANTITY_LIMIT
- Over the per-wallet purchase limit
- 429 RATE_LIMITED
- Rate limit
Create a session
Idempotency-Key header (optional): the same key with the same body returns the first result; a different body returns 409. The same partner_order_id also returns the existing session (201 when created, 200 when existing).
POST /checkout/sessions
{
"kind": "LAUNCHPAD",
"event_id": "12",
"quantity": 1,
"partner_order_id": "ORD-2026-000123",
"success_url": "https://partner.example/orders/123/done",
"cancel_url": "https://partner.example/orders/123",
"locale": "ko",
"metadata": { "campaign": "fall" }
}Fields
- kind
- LAUNCHPAD (launchpad event) or MARKET (marketplace listing)
- event_id, quantity
- LAUNCHPAD. event_id is an integer string, quantity is 1 or more (within the per-wallet limit)
- order_hash
- MARKET. Hash of a live listing (0x + 64 characters)
- partner_order_id
- Unique within your account. A–Z a–z 0–9 . _ : -, 1–64 characters
- success_url, cancel_url
- https, exactly one of your registered origins (any path)
- locale
- ko (default) or en
- metadata
- Optional. String values only, up to 20 keys. Do not put personal data in it
{
"id": "cs_5f0c…",
"url": "https://www.bankofv.co.kr/checkout/cs_5f0c…",
"status": "OPEN",
"currency": "USDT",
"total_minor": "125000000",
"expires_at": "2026-11-01T03:30:00Z",
"mode": "live"
}- Unknown fields return 400 INVALID_REQUEST.
- The price is read from the chain or the order signature and locked when you create the session. total_minor is the most the member can pay. With a tier discount the actual payment (paid_minor) can be lower.
- A session lasts 30 minutes, or until the listing expires if that comes first (MARKET).
- Send the member to url (redirect or popup).
Get a session
The create response plus the fields below.
GET /checkout/sessions/{id}
{
…,
"partner_order_id": "ORD-2026-000123",
"kind": "LAUNCHPAD", "event_id": "12", "quantity": 1, "order_hash": null,
"collection": "0x…", "token_ids": ["101"],
"paid_minor": "112500000", "failure_reason": null,
"tx_hash": "0x…", "recipient_wallet": "0x…", "chain_id": 137,
"paid_at": "2026-11-01T03:12:44Z", "created_at": "2026-11-01T03:00:00Z",
"locale": "ko", "metadata": { "campaign": "fall" }
}status
- OPEN
- Waiting for payment
- PROCESSING
- Signed and sent, waiting for the chain
- PAID
- Confirmed by a chain event (final)
- FAILED
- Failed (final). failure_reason: PRICE_CHANGED, ITEM_UNAVAILABLE, SOLD_OUT, LIMIT, TX_REVERTED
- EXPIRED
- Timed out or cancelled by the partner
No response contains a member's email or name. recipient_wallet is the receiving address, which is public on chain.
Cancel a session
POST /checkout/sessions/{id}/expire
Only before payment (OPEN, nothing in flight). An already EXPIRED session is returned as it is. The checkout.session.expired webhook is sent.
If the member's transaction was sent at almost the same moment, the session goes from EXPIRED to PAID when that transaction confirms and the completed webhook follows. Always fulfil orders from the latest state.
List items
GET /catalog/items?collection=0x…
Items on sale in your allowed collections. Leave out collection for all of them; a collection outside your list returns 403.
For display only (the indexer can be a few seconds behind). The price is read from the chain again when you create a session.
Webhooks
- checkout.session.completed
- PAID
- checkout.session.failed
- FAILED
- checkout.session.expired
- EXPIRED
- POST <webhook URL>, Content-Type: application/json.
- VBank-Signature: t=<unix seconds>,v1=<hex> — HMAC-SHA256(webhook secret, "<t>.<raw body>").
- VBank-Event-Id: evt_… — unique per event and the same on every retry. Use it to drop duplicates.
- data.total_minor is the amount actually paid (paid_minor). failed adds data.failure_reason.
- Answer with 2xx within 10 seconds. A 3xx is not followed and counts as a failure.
- On failure we retry after 1 min, 5 min, 30 min, 2 h, 6 h and 24 h (7 tries at most). If all fail, an operator checks.
- Delivery is at least once and not ordered. You may check the current state with the lookup API before acting.
{
"id": "evt_…",
"type": "checkout.session.completed",
"created_at": "2026-11-01T03:12:44Z",
"mode": "live",
"data": {
"session_id": "cs_…",
"partner_order_id": "ORD-2026-000123",
"kind": "LAUNCHPAD",
"collection": "0x…",
"token_ids": ["101"],
"total_minor": "112500000",
"currency": "USDT",
"recipient_wallet": "0x…",
"tx_hash": "0x…",
"chain_id": 137
}
}Verify the signature (Node.js)
Verify against the raw body exactly as received (before parsing) and drop any t older than 5 minutes.
import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyVBankWebhook(secret, rawBody, header, nowSec = Math.floor(Date.now() / 1000)) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=", 2)));
const t = Number(parts.t);
if (!Number.isInteger(t) || Math.abs(nowSec - t) > 300) return false;
const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest();
const given = Buffer.from(parts.v1 ?? "", "hex");
return given.length === expected.length && timingSafeEqual(given, expected);
}How payment is confirmed
- Returning to success_url is not proof of payment. Confirm an order only from the webhook or a PAID lookup.
- A session becomes PAID only when the chain event the indexer saw has the hash of the transaction sent for this session, and item, quantity and amount match.
- It is confirmed after a few blocks (an operator setting) are on top. On Polygon this usually takes 10–15 seconds.
- If something does not match, nothing is confirmed and an operator checks. After 15 minutes without a matching event the transaction is treated as not a payment.
Test mode
- vb_test_ key = Polygon Amoy (80002) + TestUSDT0. The checkout window shows a "Test" band.
- Test and live sessions never see each other, and each mode has its own webhook secret.