Skip to content
VBANK

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

  1. Ask a VBank operator to register your allowed origins (https), webhook URL (https, port 443, public IP) and the collections you sell.
  2. 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.
  3. 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.