BitPalDocs
Non-custodial crypto checkout비수탁 크립토 결제

Accept crypto. Get paid directly.크립토로 결제받고, 바로 정산.

Each order gets its own deposit address. The buyer sends stablecoins from any wallet or exchange — no wallet connection — and funds settle straight to you. No escrow, no custody.

주문마다 고유 입금 주소가 생깁니다. 구매자는 지갑 연결 없이 아무 지갑·거래소에서 스테이블코인을 보내고, 자금은 판매자에게 바로 정산됩니다. 에스크로도 수탁도 없습니다.

New here? Take a test payment end to end in about ten minutes → Quickstart.처음이신가요? 약 10분이면 테스트 결제를 끝까지 해볼 수 있습니다 → 빠른 시작.

Three ways to integrate연동 방법 3가지

  • SDK / hosted checkout — create a session in code, redirect the buyer to the returned URL. → SDK
  • Payment links — no code. Create a link, share the URL, get paid. → Payment links
  • x402 — charge per API call. The caller pays with a signed authorization and retries, no page involved. → x402
  • SDK / 호스티드 체크아웃 — 코드로 세션을 만들고 반환된 URL로 구매자를 보냅니다. → SDK
  • 결제 링크 — 코드가 필요 없습니다. 링크를 만들어 URL을 공유하면 결제를 받을 수 있습니다. → 결제 링크
  • x402 — API 호출 단위로 과금합니다. 호출자가 서명한 authorization으로 결제하고 요청을 다시 보내며, 결제 페이지는 없습니다. → x402

What you can accept지원 자산

Token토큰Networks네트워크
USDCBase · Arbitrum · Solana
USDTTron · BNB (BSC) · Solana
JPYCPolygon · Kaia

Same-chain, same-token — no bridging or swapping. Full identifiers & decimals in Chains & contracts.

같은 체인·같은 토큰으로 결제받습니다 — 브리지나 스왑이 없습니다. 전체 식별자와 decimals는 체인 & 컨트랙트에서 확인하세요.

How a payment flows결제 흐름

  1. You create a session (or link) with an amount.
  2. Buyer opens the hosted page, picks a network, sends the exact amount to a unique address.
  3. BitPal detects & confirms it, settles to you, and sends a checkout.session.paid webhook.
  1. 금액을 넣어 세션(또는 결제 링크)을 만듭니다.
  2. 구매자가 호스티드 결제 페이지에서 네트워크를 고르고, 고유 입금 주소로 정확한 금액을 보냅니다.
  3. BitPal이 입금을 감지·확정하고 판매자에게 정산한 뒤 checkout.session.paid 웹훅을 보냅니다.

Quickstart빠른 시작

Take one test payment end to end: create a session, pay it on a testnet, and receive the signed webhook. About ten minutes.

테스트 결제 한 건을 처음부터 끝까지 진행합니다. 세션을 만들고, 테스트넷에서 결제하고, 서명된 웹훅을 받습니다. 약 10분이면 됩니다.

Before you start시작하기 전에

  1. Console account in test mode. Everything below uses testnets and test keys — no real funds.
  2. Payout wallet. Console → Settings → Payout Wallet. Each deposit address is committed to this wallet, so a session cannot issue an address without one. Register an address for every network family you accept (EVM 0x…, Tron T…, Solana) — see Payout wallets below.
  3. Test API key. Console → Developers → API Keys → create a bp_test_… key. It is shown once. Keep it on your server — never in a browser or app bundle.
  4. A testnet wallet with a little gas (for example Base Sepolia ETH) to send the payment. The checkout page's test faucet mints test tokens, not gas.
  1. 테스트 모드의 콘솔 계정. 아래 과정은 모두 테스트넷과 테스트 키를 사용하며, 실제 자금은 쓰이지 않습니다.
  2. 정산 지갑. 콘솔 → 설정 → 정산 지갑. 모든 입금 주소는 이 지갑에 고정(commit)되므로, 정산 지갑이 없으면 세션이 입금 주소를 발급할 수 없습니다. 결제받을 네트워크 계열마다(EVM 0x…, Tron T…, Solana) 주소를 등록하세요 — 아래 정산 지갑을 참고하세요.
  3. 테스트 API 키. 콘솔 → 개발자 → API 키에서 bp_test_… 키를 만듭니다. 키는 한 번만 표시됩니다. 서버에만 보관하고, 브라우저나 앱 번들에는 절대 넣지 마세요.
  4. 결제를 보낼 테스트넷 지갑과 약간의 가스비(예: Base Sepolia ETH). 결제 페이지의 테스트 faucet은 가스가 아니라 테스트 토큰만 발행합니다.

Payout wallets정산 지갑

Payments settle straight to your own wallet — BitPal never holds them. You register one payout address per network family (VM) in Console → Settings → Payout Wallet. There is no API for this; it is console-only.

결제 대금은 판매자 본인의 지갑으로 바로 정산되며, BitPal은 자금을 보관하지 않습니다. 정산 주소는 네트워크 계열(VM)마다 하나씩 콘솔 → 설정 → 정산 지갑에서 등록합니다. 이 설정용 API는 없으며 콘솔에서만 할 수 있습니다.

Network family네트워크 계열Address to register등록할 주소Used for용도
EVMOne 0x… address0x… 주소 하나Every EVM network you accept (Base, Arbitrum, BNB Chain, Polygon, Kaia …) — the same address works on all of them.결제받는 모든 EVM 네트워크(Base, Arbitrum, BNB Chain, Polygon, Kaia …) — 같은 주소를 모든 네트워크에서 사용합니다.
TronA base58 address starting with T…T…로 시작하는 base58 주소Tron payments.Tron 결제.
SolanaYour wallet (owner) address — not a token account (ATA)지갑(owner) 주소 — 토큰 계정(ATA)이 아닙니다Solana payments.Solana 결제.
  • Test and live are separate. Wallets are registered per mode: switch the console to TEST to register test payout wallets, and to LIVE for live ones. A test wallet is never used for live payments, or the other way round.
  • A family without a wallet is not offered. If the buyer can choose, networks of a family with no payout wallet in that mode are left out of the payment options. If you fix the network with pay_chain (or a deposit address is requested on it), the call fails with 400 SELLER_WALLET_NOT_CONFIGURED. There is no fallback to another family or another wallet. A session created without pay_chain needs at least one payout wallet (any family) in that mode — if allowed_pay_chains / allowed_assets are set, a wallet for one of the listed networks' families. Without any network restriction, a session offers Base USDC if you have an EVM wallet, otherwise the networks of the families you registered.
  • Changing a wallet applies to deposit addresses issued afterwards. An address that was already issued is committed to the payout wallet of that moment (the commitment is part of the address), so a payment to it still settles there.
  • 테스트와 라이브는 분리되어 있습니다. 지갑은 모드별로 등록합니다. 테스트 정산 지갑은 콘솔을 TEST로, 라이브 정산 지갑은 LIVE로 전환한 뒤 등록하세요. 테스트 지갑이 라이브 결제에 쓰이거나 그 반대가 되는 일은 없습니다.
  • 지갑이 없는 계열은 결제 옵션에 나오지 않습니다. 구매자가 네트워크를 고를 수 있는 경우, 해당 모드에 정산 지갑이 없는 계열의 네트워크는 결제 옵션에서 빠집니다. pay_chain으로 네트워크를 고정했거나 그 네트워크로 입금 주소를 요청하면 400 SELLER_WALLET_NOT_CONFIGURED로 실패합니다. 다른 계열이나 다른 지갑으로 대체되지 않습니다. pay_chain 없이 만드는 세션은 해당 모드에 정산 지갑이 계열과 무관하게 하나 이상 있어야 합니다(allowed_pay_chains / allowed_assets를 지정했다면 그중 한 네트워크 계열의 지갑). 네트워크 제한이 없는 세션은 EVM 지갑이 있으면 Base USDC를, 없으면 등록한 계열의 네트워크를 결제 옵션으로 제시합니다.
  • 지갑을 변경하면 그 이후 발급되는 입금 주소부터 적용됩니다. 이미 발급된 주소는 발급 시점의 정산 지갑에 고정되어 있으므로(주소 자체에 포함됨), 그 주소로 들어온 결제는 계속 기존 지갑으로 정산됩니다.

1. Create a checkout session1. 결제 세션 만들기

Install the SDK, or call the REST API directly.

SDK를 설치하거나 REST API를 직접 호출합니다.

npm install @bitpal/checkout
import { BitPal } from '@bitpal/checkout';

const bitpal = new BitPal(process.env.BITPAL_API_KEY); // bp_test_…

const { data: session } = await bitpal.checkout.createSession({
  line_items: [{ name: 'Pro plan', amount: '29', currency: 'USDC' }],
  pay_chain: 'eip155:84532',            // Base Sepolia (test key → testnet values only)
  expires_in_minutes: 30,               // payment window, 1–60 (default 15)
  success_url: 'https://your.store/thanks?session={CHECKOUT_SESSION_ID}',
});
// store session.id with your order, then send the buyer to session.checkout_url

The same request over HTTP:

같은 요청을 HTTP로 보내면 다음과 같습니다.

curl -X POST https://api.bitpal.io/v1/checkout/sessions \
  -H "Authorization: Bearer $BITPAL_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-1024" \
  -d '{
    "line_items": [{ "name": "Pro plan", "amount": "29", "currency": "USDC" }],
    "pay_chain": "eip155:84532",
    "expires_in_minutes": 30,
    "success_url": "https://your.store/thanks?session={CHECKOUT_SESSION_ID}"
  }'

Response 201:

응답 201:

{
  "data": {
    "id": "cs_Q2FxKhomCEju_OqvKfrQsTOh",
    "checkout_url": "https://bitpal.io/pay/cs_Q2FxKhomCEju_OqvKfrQsTOh?token=<session_token>",
    "session_token": "<session_token>",
    "status": "created",
    "amount_total": "29000000",
    "currency": "USDC",
    "expires_at": "2026-11-06T07:58:28.776Z",
    "tax": { "tax_amount": "0", "total_with_tax": "29000000" }
  },
  "meta": { "mode": "TEST", "requestId": "ec091faf-…" }
}
  • amount is a plain number string in the line item's currency — no decimals to compute. amount_total comes back as a 6-decimal integer string ("29000000" = 29).
  • Minimum: the session total (sum of amount × quantity) must be at least 1.00 in the session currency ($1 for USDC/USDT, ¥1 for JPYC), otherwise 400 CHECKOUT_AMOUNT_TOO_LOW. Some networks have a higher minimum — Tron currently $300 (testnet included), JPYC ¥100 on every network; such a network is not offered for a smaller total (or 400 CHECKOUT_BELOW_MIN_PAYMENT if you fixed it with pay_chain).
  • Idempotency-Key (8–128 chars) makes retries safe: the same key and body within 24 hours returns the original response with X-Idempotency-Replayed: true.
  • {CHECKOUT_SESSION_ID} in success_url is replaced with the session id when the buyer returns.
  • amount는 line item의 통화 단위로 쓴 일반 숫자 문자열입니다 — decimals를 계산할 필요가 없습니다. amount_total은 소수점 6자리 정수 문자열로 반환됩니다("29000000" = 29).
  • 최소 금액: 세션 총액(amount × quantity의 합)은 세션 통화 기준 1.00 이상이어야 합니다(USDC/USDT는 $1, JPYC는 ¥1). 미만이면 400 CHECKOUT_AMOUNT_TOO_LOW가 반환됩니다. 일부 네트워크는 최소 금액이 더 높습니다 — 현재 Tron은 $300(테스트넷 포함), JPYC는 모든 네트워크에서 ¥100입니다. 총액이 그보다 작으면 해당 네트워크는 결제 옵션에 나오지 않습니다(pay_chain으로 고정했다면 400 CHECKOUT_BELOW_MIN_PAYMENT).
  • Idempotency-Key(8–128자)를 보내면 재시도가 안전해집니다. 24시간 안에 같은 키와 같은 본문으로 다시 요청하면 원래 응답이 X-Idempotency-Replayed: true와 함께 반환됩니다.
  • success_url의 {CHECKOUT_SESSION_ID}는 구매자가 돌아올 때 세션 id로 바뀝니다.

2. Send the buyer to the checkout page2. 구매자를 결제 페이지로 보내기

Redirect to checkout_url, or open it in a popup so the buyer stays on your page (see Popup & headless). On the page the buyer:

checkout_url로 리다이렉트하거나, 구매자가 페이지를 떠나지 않도록 팝업으로 엽니다(팝업 & 헤드리스 참고). 결제 페이지에서 구매자는 다음을 진행합니다.

  1. picks a network and token (if you allowed more than one),
  2. enters a refund address — over/under-payments are returned there,
  3. gets a one-time deposit address and the exact amount, and sends it from any wallet or exchange.
  1. 네트워크와 토큰을 고릅니다(둘 이상 허용한 경우).
  2. 환불 주소를 입력합니다 — 과다·과소 입금액은 이 주소로 반환됩니다.
  3. 일회용 입금 주소와 정확한 금액을 받고, 아무 지갑이나 거래소에서 송금합니다.

In test mode the page shows Get test assets: it mints test tokens to the buyer's wallet so you can complete a real on-chain payment.

테스트 모드에서는 결제 페이지에 테스트 자산 받기가 표시됩니다. 구매자 지갑으로 테스트 토큰을 발행해 주므로 실제 온체인 결제를 끝까지 해볼 수 있습니다.

No testnet wallet? Console → Developers → Sandbox creates a session and sends the deposit for you — pay in full, underpay, overpay or send the wrong token — and shows every webhook event as your endpoint receives it. Test mode only; 10 simulations per minute and 300 per day per account.
테스트넷 지갑이 없나요? 콘솔 → 개발자 → 샌드박스가 세션을 만들고 입금까지 대신 보내 줍니다 — 전액, 과소 입금, 과다 입금, 잘못된 토큰 전송을 고를 수 있고, 엔드포인트가 받는 웹훅 이벤트를 모두 보여 줍니다. 테스트 모드 전용이며, 계정당 분당 10회·하루 300회까지 실행할 수 있습니다.

3. Receive the webhook3. 웹훅 받기

Console → Developers → Webhooks → Add Endpoint. The URL must be publicly reachable over HTTPS (for local development use a tunnel such as ngrok or cloudflared). Copy the signing secret (whsec_…) — it is shown once.

콘솔 → 개발자 → 웹훅 → 엔드포인트 추가. URL은 HTTPS로 외부에서 접근할 수 있어야 합니다(로컬 개발 중이라면 ngrok, cloudflared 같은 터널을 쓰세요). 서명 시크릿(whsec_…)은 한 번만 표시되니 복사해 두세요.

import express from 'express';
import { verifyWebhookSignature, parseWebhookEvent, WEBHOOK_HEADERS } from '@bitpal/checkout';

const app = express();

app.post('/webhooks/bitpal', express.raw({ type: 'application/json' }), async (req, res) => {
  const raw = req.body.toString('utf8');                     // exact bytes BitPal signed
  const ok = await verifyWebhookSignature(                   // async — always await it
    raw,
    req.header(WEBHOOK_HEADERS.signature) ?? '',
    process.env.BITPAL_WEBHOOK_SECRET,                       // whsec_…
    { timestamp: req.header(WEBHOOK_HEADERS.timestamp) ?? '' },
  );
  if (!ok) return res.status(400).send('invalid signature');

  const event = parseWebhookEvent(raw);
  if (await alreadyProcessed(event.id)) return res.sendStatus(200); // at-least-once delivery

  if (event.event === 'checkout.session.paid') {
    await fulfilOrder(event.data.session_id, { late: event.data.late === true });
  }
  await markProcessed(event.id);
  res.sendStatus(200);                                       // any 2xx within 10 s = delivered
});

For one successful payment you receive, in order: checkout.session.created → checkout.session.deposit_detected → checkout.session.paid → checkout.session.settled. deposit_detected is not guaranteed: paid can arrive without it (a late payment, or a deposit that was already fully confirmed when BitPal first saw it), so never wait for it before fulfilling. Use Send Test in the console to check your endpoint before paying.

결제 한 건이 성공하면 다음 순서로 이벤트를 받습니다: checkout.session.created → checkout.session.deposit_detected → checkout.session.paid → checkout.session.settled. deposit_detected는 보장되지 않습니다. 늦은 결제이거나 BitPal이 처음 확인했을 때 이미 블록 확정이 끝난 입금이면 paid가 단독으로 올 수 있으니, 주문 처리 전에 이 이벤트를 기다리지 마세요. 결제 전에 콘솔의 테스트 전송으로 엔드포인트를 먼저 확인할 수 있습니다.

4. Fulfil on checkout.session.paid4. checkout.session.paid에서 주문 처리하기

  • paid means the deposit reached the required confirmations — safe to fulfil. settled follows when funds are swept to your payout wallet.
  • Treat the webhook (or a server-side getSession(id)) as the source of truth. The redirect to success_url and popup messages are hints for your UI only.
  • Check data.late: true means the full amount arrived after the payment window (accepted for up to 7 days). Decide whether to fulfil or refund it from the console — or create sessions with "late_payments": "refund" so late payments are refunded automatically instead (Payment lifecycle).
  • paid는 입금이 필요한 블록 확정 수에 도달했다는 뜻입니다 — 주문을 처리해도 안전합니다. 이어서 자금이 정산 지갑으로 옮겨지면 settled가 옵니다.
  • 웹훅(또는 서버에서 호출한 getSession(id))을 기준으로 판단하세요. success_url 리다이렉트와 팝업 메시지는 UI용 힌트일 뿐입니다.
  • data.late를 확인하세요. true는 결제 기한이 지난 뒤 전액이 도착했다는 뜻입니다(최대 7일까지 인정). 주문을 처리할지, 콘솔에서 환불할지 판단하세요. 늦은 결제를 아예 받지 않으려면 세션을 "late_payments": "refund"로 만들어 자동 환불되게 할 수 있습니다(결제 생애주기).

Going live라이브 전환

  1. Create a bp_live_… key and switch chain values to mainnet (Chains & contracts).
  2. Register live payout wallets — test and live are separate environments.
  3. Add the webhook endpoint again in live mode. It gets its own whsec_… secret.
  4. Handle the non-happy paths: checkout.session.unresolved, checkout.session.expired, checkout.refund.recorded and late payments (Payment lifecycle).
  1. bp_live_… 키를 만들고 체인 값을 메인넷으로 바꿉니다(체인 & 컨트랙트).
  2. 라이브 정산 지갑을 등록합니다 — 테스트와 라이브는 별도 환경입니다.
  3. 라이브 모드에서 웹훅 엔드포인트를 다시 추가합니다. 별도의 whsec_… 시크릿이 발급됩니다.
  4. 정상 흐름 외의 경우도 처리하세요: checkout.session.unresolved, checkout.session.expired, checkout.refund.recorded, 늦은 결제(결제 생애주기).

SDK

Create checkout sessions from your backend and redirect buyers to the hosted page.

백엔드에서 결제 세션을 만들고 구매자를 호스티드 결제 페이지로 보냅니다.

Install & create a session설치 & 세션 생성

npm install @bitpal/checkout
import { BitPal } from '@bitpal/checkout';
const bitpal = new BitPal(process.env.BITPAL_API_KEY); // bp_test_… or bp_live_…

// A CAIP-2 id is absolute: 'eip155:8453' (Base mainnet) works only with a live key —
// with a test key it fails with CHAIN_MODE_MISMATCH. Use 'eip155:84532' for test, or a short name.
const { data: session } = await bitpal.checkout.createSession({
  line_items: [{ name: 'Pro plan', amount: '29', currency: 'USDC' }],
  pay_chain: 'base',                 // short name → Base Sepolia with bp_test_, Base with bp_live_
  expires_in_minutes: 30,                // payment window 1–60, default 15
  success_url: 'https://your.store/thanks?s={CHECKOUT_SESSION_ID}',
});
redirect(session.checkout_url);

Amounts — just pass the number금액 — 숫자만 넣으세요

Set amount to a plain number ("29", "0.5") in the line item's currency. No decimals to compute.

amount에는 line item currency 기준의 일반 숫자("29", "0.5")를 넣습니다. decimals를 계산할 필요가 없습니다.

You never compute token decimals.토큰 decimal 계산은 필요 없습니다. The amount is quoted in the line item's currency — USDC/USDT mean USD, JPYC means JPY (no FX conversion). It is independent of chain/token decimals: USDC (6dp) on Base or USDT (18dp) on BNB take the same amount, and BitPal rescales at deposit time. (Already have 6-decimal integers? Pass amount_atomic instead — the two are mutually exclusive.) 금액은 line item의 currency 기준입니다 — USDC/USDT는 USD, JPYC는 JPY를 뜻하며 환율 변환은 없습니다. 체인·토큰의 decimals와도 무관합니다. Base의 USDC(6dp)든 BNB의 USDT(18dp)든 같은 amount를 넣으면 되고, BitPal이 입금 시점에 단위를 환산합니다. (이미 소수점 6자리 정수 값이 있다면 amount_atomic을 대신 쓰세요 — 두 필드는 함께 쓸 수 없습니다.)

Example: buyer chooses chain & token예시: 구매자가 체인·토큰 선택

await bitpal.checkout.createSession({
  line_items: [{ name: 'Order #1024', amount: '49.90', currency: 'USDC' }], // currency = price denomination
  allowed_assets: [                            // (chain, token) pairs — the buyer picks one; mix tokens freely
    { chain: 'base',     token: 'USDC' },
    { chain: 'arbitrum', token: 'USDC' },
    { chain: 'tron',     token: 'USDT' },
    { chain: 'solana',   token: 'USDC' },
  ],
  // shorthand for one token across chains: allowed_pay_chains: ['base','arbitrum','solana']
});
Payment options결제 옵션 allowed_assets (chain×token pairs) is the standard way — the buyer picks any listed pair. chain takes a short-name ('base', 'tron', 'solana') or a CAIP-2 id and resolves to your key's mode (test/live). Shorthands: allowed_pay_chains (one token across chains) and pay_chain (single fixed chain). allowed_assets takes precedence. allowed_assets(체인×토큰 쌍)가 기본 방식입니다 — 구매자는 나열된 쌍 중 하나를 고릅니다. chain에는 short name('base', 'tron', 'solana')이나 CAIP-2 id를 넣으며, API 키의 모드(테스트/라이브)에 맞게 해석됩니다. 축약형으로 allowed_pay_chains(한 토큰을 여러 체인에서)와 pay_chain(체인 하나로 고정)이 있습니다. 함께 지정하면 allowed_assets가 우선합니다.

Example: poll status예시: 상태 폴링

const { data } = await bitpal.checkout.getStatus(sessionId);
if (data.status === 'paid_confirmed' || data.status === 'swept') showSuccess(); // UI only — fulfil from the webhook

Status values: created → awaiting_deposit → deposit_detected → paid_confirmed → swept, plus expired, unresolved_underpaid, wrong_token. (Sessions opened through the legacy wallet-connect flow may report pending_payment; treat it like created.) See Payment lifecycle.

상태값: created → awaiting_deposit → deposit_detected → paid_confirmed → swept, 그 밖에 expired, unresolved_underpaid, wrong_token이 있습니다. (예전 지갑 연결 방식으로 열린 세션은 pending_payment를 반환할 수 있으며, created와 같이 취급하면 됩니다.) 자세한 내용은 결제 생애주기를 참고하세요.

Method reference메서드 레퍼런스

MethodPurpose용도
createSession(params)Create a session → checkout_url.세션을 만들고 checkout_url을 받습니다.
getSession(id)Full session (authenticated).세션 전체 정보(인증 필요).
getPublicSession(id)Buyer-facing: payment options, status.구매자용 정보: 결제 옵션, 상태.
getStatus(id)Lightweight status for polling.폴링용 경량 상태 조회.
previewFee(amount)Fee preview before charging.결제 전에 수수료를 미리 계산합니다.
products / pricesCatalog: reusable products & prices (for payment links).카탈로그: 재사용할 수 있는 상품과 가격(결제 링크용).
paymentLinksCreate & manage shareable payment links from a price.가격(price)으로 공유용 결제 링크를 만들고 관리합니다.

Amount helpers (advanced, for amount_atomic): toAtomic(v, decimals) / fromAtomic(v, decimals).

금액 헬퍼(고급, amount_atomic용): toAtomic(v, decimals) / fromAtomic(v, decimals).

Popup & headless팝업 & 헤드리스

Choose how the buyer sees the payment. Every flow ends the same way: you fulfil from the webhook.

구매자에게 결제 화면을 어떻게 보여 줄지 고릅니다. 어떤 방식이든 마무리는 같습니다 — 주문 처리는 웹훅을 기준으로 합니다.

Choose a flow방식 고르기

Flow방식Buyer experience구매자 경험You build구현할 것
Redirect리다이렉트Leaves your site for the hosted page, returns to success_url.판매자 사이트를 떠나 호스티드 결제 페이지로 이동했다가 success_url로 돌아옵니다.Create a session, redirect. Simplest.세션을 만들고 리다이렉트합니다. 가장 간단합니다.
Popup팝업Hosted page in a small window; your page stays open.호스티드 결제 페이지가 작은 창으로 열리고, 판매자 페이지는 그대로 유지됩니다.Open the window, listen for one message.창을 열고 메시지 하나를 수신합니다.
Headless헤드리스Your own payment UI: options, address, QR, status.직접 만든 결제 UI: 결제 옵션, 주소, QR, 상태.A few backend endpoints plus your UI.백엔드 엔드포인트 몇 개와 결제 UI.
Payment link결제 링크A shareable URL; no code. → Payment links공유할 수 있는 URL, 코드 불필요. → 결제 링크Nothing.없음.
x402No page — an API client pays per call. → x402페이지 없음 — API 클라이언트가 호출마다 결제합니다. → x402Middleware on the route.라우트에 미들웨어 추가.

Popup팝업

Browsers block window.open unless it runs directly in the click handler. Open an empty window first, then point it at the checkout URL once your backend has created the session.

브라우저는 클릭 핸들러 안에서 바로 실행되지 않은 window.open을 차단합니다. 먼저 빈 창을 열어 두고, 백엔드가 세션을 만든 뒤 그 창을 결제 URL로 이동시키세요.

async function onPayClick() {
  // 1) open synchronously, inside the click handler
  const popup = window.open('', 'bitpal_checkout', 'width=460,height=720');

  // 2) your backend creates the session (secret key stays on the server)
  const { checkout_url, session_id } = await fetch('/api/checkout', { method: 'POST' }).then((r) => r.json());

  // 3) navigate the popup — or fall back to a full redirect if it was blocked
  if (popup && !popup.closed) popup.location.href = checkout_url;
  else window.location.href = checkout_url;

  const origin = new URL(checkout_url).origin;
  window.addEventListener('message', function onMessage(e) {
    if (e.origin !== origin) return;                       // trust only the checkout page
    const m = e.data;
    if (!m || m.source !== 'bitpal-checkout' || m.type !== 'terminal') return;
    window.removeEventListener('message', onMessage);
    refreshOrderFromServer(session_id);                     // m.step: done | expired | error
  });
}

When the payment reaches a final screen, the checkout page posts one message to its opener:

결제가 최종 화면에 도달하면 결제 페이지가 자신을 연 창(opener)으로 메시지를 하나 보냅니다.

{ "source": "bitpal-checkout", "type": "terminal", "step": "done", "session_id": "cs_…" }
  • step is done, expired or error. One terminal message is sent per final screen.
  • If the buyer sends the wrong token, the page posts { "type": "status", "step": "wrong_token" } instead. It is not final: the wrong token is returned and the session can still be paid with the right one, so keep listening for terminal.
  • The message is a UI hint. Re-read the order from your server (which learned the result from the webhook) before showing success.
  • Messages can be lost (the buyer closes the window, browser privacy settings). Keep a fallback: poll your own backend for the order status.
  • Don't close the popup yourself — the completion screen lets the buyer email a receipt. The popup closes itself only after the buyer sends that receipt; otherwise the buyer closes it with the Close button, so don't wait for the window to close.
  • step은 done, expired, error 중 하나입니다. 최종 화면마다 terminal 메시지가 한 번 전송됩니다.
  • 구매자가 잘못된 토큰을 보내면 대신 { "type": "status", "step": "wrong_token" }이 전송됩니다. 이는 최종 상태가 아닙니다. 잘못 보낸 토큰은 반환되고 올바른 토큰으로 계속 결제할 수 있으니, terminal 메시지를 계속 기다리세요.
  • 이 메시지는 UI 힌트입니다. 성공 화면을 보여 주기 전에 서버(웹훅으로 결과를 받은)에서 주문 상태를 다시 읽으세요.
  • 메시지는 유실될 수 있습니다(구매자가 창을 닫거나 브라우저 개인정보 설정 등). 대비책으로 자체 백엔드에서 주문 상태를 폴링하세요.
  • 팝업을 직접 닫지 마세요 — 완료 화면에서 구매자가 영수증을 이메일로 받을 수 있습니다. 팝업은 구매자가 영수증을 보낸 뒤에만 스스로 닫히고, 그 밖에는 구매자가 닫기 버튼으로 닫습니다. 창이 닫히기를 기다리지 마세요.

Headless (your own UI)헤드리스 (직접 만든 UI)

Use this when you render the payment screen yourself. The buyer still pays to a per-order deposit address; you just display it.

결제 화면을 직접 렌더링할 때 사용합니다. 구매자는 여전히 주문별 입금 주소로 결제하며, 판매자는 그 주소를 화면에 보여 주기만 하면 됩니다.

Call BitPal from your backend. Browsers on your domain may only call the public session endpoint directly (CORS). Issue the deposit address and poll status through your server, and never ship a secret key to the browser.
BitPal은 백엔드에서 호출하세요. 판매자 도메인의 브라우저에서 직접 호출할 수 있는 것은 공개 세션 엔드포인트뿐입니다(CORS). 입금 주소 발급과 상태 폴링은 서버를 거치고, 시크릿 키는 절대 브라우저로 보내지 마세요.
  1. Create the session on your server (as in the Quickstart). Keep id and session_token.서버에서 세션을 만듭니다(빠른 시작과 동일). id와 session_token을 보관하세요.
  2. Show the payment options. getPublicSession(id) returns payment_options — one entry per allowed (chain, token) with caip2, token_symbol, decimals and the exact amount_atomic.결제 옵션을 보여 줍니다. getPublicSession(id)는 payment_options를 반환합니다. 허용된 (체인, 토큰)마다 항목이 하나씩 있으며 caip2, token_symbol, decimals, 정확한 amount_atomic이 담겨 있습니다.
  3. Collect a refund address from the buyer, in the format of the chosen network (EVM 0x…, Tron T…, Solana base58). It is committed into the deposit address and cannot change.구매자에게서 선택한 네트워크 형식(EVM 0x…, Tron T…, Solana base58)의 환불 주소를 받습니다. 환불 주소는 입금 주소에 고정되며 바꿀 수 없습니다.
  4. Issue the deposit address (backend):입금 주소를 발급합니다(백엔드):
    const { data } = await bitpal.checkout.issueDepositAddress(session.id, {
      session_token: session.session_token,
      refundAddress: '0xBuyerRefundWallet…',
      chain: 'eip155:84532',
      token: 'USDC',
    });
    // {
    //   "sessionId": "cs_…",
    //   "chain": "eip155:84532",
    //   "depositAddress": "0x6b19e871c934aB52775B6712e110f3e4b0626347",
    //   "expectedToken": "0x943b94dacf1507f2658f1cfe103ac36ebc13ef65",
    //   "expectedAmount": "29000000",     // in the token's own decimals
    //   "feeAmount": "435000",
    //   "refundAddress": "0xa0Ee7A142d267C1f36714E4a8F75612F20a79720",
    //   "deadline": "1793957308",       // on-chain deadline (unix s) = expiresAt + 5400 (90-minute grace)
    //   "expiresAt": "2026-11-06T07:58:28.776Z"
    // }
  5. Display the address, a QR code and the exact amount — format expectedAmount with the token's decimals (6 for USDC, 18 for BNB USDT and JPYC). Show the countdown to expiresAt.주소, QR 코드, 정확한 금액을 표시합니다 — expectedAmount는 토큰의 decimals(USDC 6, BNB USDT·JPYC 18)로 변환해 보여 주세요. expiresAt까지 남은 시간도 표시합니다.
  6. Track status through your backend (getStatus(id)) for the UI, and fulfil from the webhook.UI용 상태는 백엔드를 통해 확인하고(getStatus(id)), 주문 처리는 웹훅을 기준으로 합니다.

Calling issueDepositAddress again for the same session returns the same address and keeps the first refund address — a later call cannot change it. A deposit address belongs to one order; don't reuse it.

같은 세션에 issueDepositAddress를 다시 호출하면 같은 주소가 반환되고 처음 입력한 환불 주소가 유지됩니다 — 이후 호출로는 바꿀 수 없습니다. 입금 주소는 주문 하나에만 속하므로 재사용하지 마세요.

x402

Charge per API call. The caller pays with a signed authorization and retries — no checkout page, no redirect.

API 호출 단위로 과금합니다. 호출자가 서명으로 결제하고 재요청 — 결제 페이지도 리다이렉트도 없습니다.

How it works동작 방식

  1. An unpaid request to your route returns 402 with the payment terms.
  2. The caller signs an EIP-3009 authorization (gasless — signature only) and retries with an X-PAYMENT header.
  3. BitPal verifies the signature and broadcasts it. Funds move from the payer through a per-terms settlement address that pays you and the fee wallet in the same transaction — BitPal never holds them.
  4. Your handler runs and the response carries X-PAYMENT-RESPONSE with the transaction hash.
  1. 결제되지 않은 요청에는 라우트가 402와 결제 조건을 반환합니다.
  2. 호출자가 EIP-3009 authorization에 서명하고(가스 불필요 — 서명만) X-PAYMENT 헤더를 붙여 다시 요청합니다.
  3. BitPal이 서명을 검증하고 브로드캐스트합니다. 자금은 결제자에서 결제 조건별 정산 주소로 이동하고, 같은 트랜잭션 안에서 판매자 정산 지갑과 수수료 지갑으로 나뉘어 지급됩니다 — BitPal은 자금을 보관하지 않습니다.
  4. 핸들러가 실행되고, 응답에 트랜잭션 해시가 담긴 X-PAYMENT-RESPONSE가 붙습니다.
Payments settle to the same payout wallet as checkout. Register one in the console first — x402 has no separate wallet setting.결제는 체크아웃과 동일한 정산 지갑으로 들어옵니다. 콘솔에서 먼저 등록하세요 — x402 전용 지갑 설정은 없습니다.

Add it to a route라우트에 붙이기

npm install @bitpal/checkout
import { x402 } from '@bitpal/checkout';

// $1.00 USDC per call
app.get('/report', x402({
  apiKey: process.env.BITPAL_API_KEY,   // bp_test_… or bp_live_…
  payTo: '0xYourPayoutWallet', // the payout wallet registered in Console for this mode — anything else: PAYTO_NOT_MERCHANT_WALLET
  network: 'base',                  // test key → 'base-sepolia'
  amount: '1.00',                    // 사람 표기 — decimals 계산 불필요
}), (req, res) => {
  res.json({ report: '…' });
});

Other runtimes — Hono, Fastify, Next.js다른 런타임 — Hono, Fastify, Next.js

The x402() adapter above is Express-shaped. Everywhere else, use X402Gate directly — it takes the incoming header and hands back what to send. Same logic, no framework assumptions.

위 x402() 어댑터는 Express 모양입니다. 그 외 런타임은 X402Gate 를 직접 쓰세요 — 들어온 헤더를 넣으면 무엇을 응답할지 돌려줍니다. 로직은 같고 프레임워크 가정만 없습니다.

import { X402Gate, createMemoryReplayStore, X402_PAYMENT_RESPONSE_HEADER } from '@bitpal/checkout';

// 반드시 모듈 스코프에 하나 — 아래 경고 참조
const replayStore = createMemoryReplayStore();

const gate = new X402Gate({
  apiKey: process.env.BITPAL_API_KEY,
  payTo: '0xYourPayoutWallet',
  network: 'base',
  amount: '1.00',
  replayStore,
});

// Hono 예시 — Next.js Route Handler 도 모양이 같다
app.get('/report', async (c) => {
  const url = new URL(c.req.url);
  // 결제는 이 문자열에 묶인다. 402 와 재요청에서 같은 값이어야 한다(쿼리 제외).
  const resource = `${url.origin}${url.pathname}`;

  const result = await gate.collect({ header: c.req.header('x-payment'), resource });
  if (result.kind !== 'settled') {
    // 402(미결제) · 409(재사용) · 503(미확정) 을 그대로 옮긴다
    return c.json(result.body, result.status);
  }

  c.header(X402_PAYMENT_RESPONSE_HEADER, result.paymentResponseHeader);
  return c.json({ report: '…' });   // 결제된 요청만 여기 도달
});
Create the replay store once, at module scope. The gate uses it to remember which authorizations have already been served — that is what makes one payment unlock exactly one response. Build it inside the handler and every request gets a fresh, empty memory, so the same X-PAYMENT can be replayed for more calls. (Your funds are never at risk — BitPal settles a given authorization once and the on-chain nonce is spent — but you would be giving the resource away.) Running more than one instance? Back it with a shared store: X402ReplayStore is an interface, so Redis or a table works.replay store 는 모듈 스코프에 딱 하나 만드세요. 게이트가 "이미 내준 결제" 를 여기에 기억합니다 — 그게 결제 1건이 응답 1건만 여는 근거입니다. 핸들러 안에서 만들면 요청마다 빈 기억이 새로 생겨 같은 X-PAYMENT 으로 계속 호출됩니다. (자금은 안전합니다 — BitPal 은 한 authorization 을 한 번만 정산하고 온체인 nonce 도 이미 소진됩니다 — 다만 리소스를 공짜로 내주게 됩니다.) 인스턴스를 여러 대 띄운다면 공유 저장소로 바꾸세요: X402ReplayStore 는 인터페이스라 Redis 나 테이블로 구현하면 됩니다.

The 402 response402 응답

This is standard x402 — a stock client such as x402-fetch can pay it. payTo is not your wallet: it is a CREATE2 address derived from the payment terms, which is what lets the caller sign a single authorization and still have the fee split correctly. feeBreakdown is a BitPal extension and is informational — a caller may ignore it and still pay correctly.

x402 표준 그대로라 x402-fetch 같은 순정 클라이언트도 결제할 수 있습니다. payTo 는 정산 지갑이 아닙니다 — 결제 조건에서 유도된 CREATE2 주소이고, 그 덕에 호출자가 authorization 1건만 서명해도 수수료가 정확히 나뉩니다. feeBreakdown 은 BitPal 확장이며 참고용입니다 — 호출자가 무시해도 정상 결제됩니다.

Do not hand-roll this body. payTo is computed on-chain from the terms, so only BitPal can produce a valid one. Without the middleware, call POST /v1/x402/requirements and return what it gives you — then /verify and /settle with the caller's X-PAYMENT.이 바디를 직접 만들지 마세요. payTo 는 결제 조건에서 온체인으로 계산되는 값이라 BitPal 만 유효한 값을 만들 수 있습니다. 미들웨어를 쓰지 않는다면 POST /v1/x402/requirements 를 호출해 받은 것을 그대로 반환하고, 호출자의 X-PAYMENT 로 /verify → /settle 하세요.
{
  "x402Version": 1,
  "accepts": [{
    "scheme": "exact",
    "network": "base",
    "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
    // 정산 지갑이 아니라 조건에서 유도된 정산 주소 (CREATE2)
    "payTo": "0xSettlementAddressDerivedFromTerms",
    // gross — 수수료 차감 전 총액. 호출자는 이 금액을 payTo 로 서명한다.
    "maxAmountRequired": "1000000",
    "resource": "https://your.api/report",
    "extra": { "name": "USD Coin", "version": "2", "chainId": 8453 },
    "feeBreakdown": {
      "netAmount": "985000",
      "feeAmount": "15000",
      "feeBps": 150,
      "feeRecipient": "0x…"
    }
  }]
}

Rules worth knowing알아둘 규칙

  • Exact amounts only. Over- and underpayment are both rejected — there is no refund path on this lane.
  • One payment, one response. A given authorization unlocks a single response; replaying it does not buy another call.
  • Bound to the resource. A payment verified for one route cannot be settled against another.
  • EOA signers only. Smart-contract wallets cannot sign EIP-3009 authorizations.
  • Standard x402 payload. The caller signs one authorization — any x402 client works, including ones that have never heard of BitPal.
  • If settle times out, poll the payment status instead of retrying blindly — the call is idempotent either way.
  • 정확한 금액만. 과다·과소 지급 모두 거부됩니다 — 이 레인엔 환불 경로가 없습니다.
  • 결제 1건 = 응답 1건. 한 authorization 은 한 번의 응답만 열어줍니다. 재전송해도 추가 호출이 되지 않습니다.
  • 리소스에 묶입니다. 한 라우트로 검증된 결제를 다른 라우트에 정산할 수 없습니다.
  • EOA 서명자만. 스마트컨트랙트 지갑은 EIP-3009 서명을 만들 수 없습니다.
  • x402 표준 payload. 호출자는 authorization 1건만 서명합니다 — BitPal 을 모르는 x402 클라이언트도 그대로 결제할 수 있습니다.
  • settle 이 타임아웃되면 무작정 재시도하지 말고 결제 상태를 조회하세요 — 어느 쪽이든 멱등합니다.

When settle is uncertain정산이 불확실할 때

If the chain is slow or the broadcast result is unknown, the gate returns 503 instead of your resource. Do not retry blindly — the payment may well have gone through. You have two ways to learn the outcome, and they carry the same answer:

체인이 느리거나 브로드캐스트 결과가 불확실하면 게이트가 리소스 대신 503 을 돌려줍니다. 무작정 재시도하지 마세요 — 결제는 이미 나갔을 수 있습니다. 결말을 아는 방법이 둘이고, 답은 같습니다:

  • Poll — GET /v1/x402/payments/:id with the paymentId from the 503 body.폴링 — 503 바디의 paymentId 로 GET /v1/x402/payments/:id.
  • Webhook — x402.payment.resolved is pushed once BitPal settles the question, so you do not have to sit on a polling loop.웹훅 — BitPal 이 결말을 내면 x402.payment.resolved 가 날아옵니다. 폴링 루프를 붙들고 있을 필요가 없습니다.
Record the paymentId when you get a 503. It is how you tie the later webhook back to the caller who is waiting — and how you answer them if they ask.503 을 받으면 paymentId 를 남겨 두세요. 나중에 오는 웹훅을 기다리던 호출자와 연결하는 열쇠이고, 문의가 왔을 때 답할 근거입니다.

Supported지원 범위

Network네트워크 Token토큰 network
BaseUSDCbase
Base SepoliaUSDCbase-sepolia

Test keys accept only base-sepolia; live keys only base. More chains follow the same flow as they are enabled — see Chains & contracts.

테스트 키는 base-sepolia 만, 라이브 키는 base 만 받습니다. 체인이 추가되면 동일한 흐름을 따릅니다 — 체인 & 컨트랙트 참조.

Payment lifecycle결제 생애주기

What a session goes through, what each state means for you, and how amounts, late payments and refunds work.

세션이 거치는 상태와 각 상태에서 판매자가 할 일, 그리고 금액·늦은 결제·환불이 어떻게 처리되는지 설명합니다.

States상태

created ──▶ awaiting_deposit ──▶ deposit_detected ──▶ paid_confirmed ──▶ swept
               │      ▲   │                 │
               │      │   └─▶ unresolved_underpaid ──(deadline)──▶ expired  (partial refund)
               │      └────── wrong_token  (wrong asset returned; still payable)
               └──(deadline, nothing paid)──▶ expired
Status상태Meaning의미What you do판매자가 할 일
createdSession exists; the buyer has not picked a payment option yet.세션이 생성되었고, 구매자가 아직 결제 옵션을 고르지 않았습니다.Nothing.없음.
awaiting_depositA deposit address was issued; waiting for funds.입금 주소가 발급되었고, 입금을 기다리는 중입니다.Nothing.없음.
deposit_detectedFunds seen on-chain, waiting for confirmations.온체인에서 입금이 확인되었고, 블록 확정을 기다리는 중입니다.Show "processing". Don't fulfil yet."처리 중"을 표시합니다. 아직 주문을 처리하지 마세요.
paid_confirmedThe full amount is confirmed.전액이 확정되었습니다.Fulfil (webhook checkout.session.paid).주문을 처리합니다(웹훅 checkout.session.paid).
sweptFunds moved to your payout wallet, minus the fee.수수료를 뺀 자금이 정산 지갑으로 이동했습니다.Reconcile (webhook checkout.session.settled).정산 내역을 대사(reconcile)합니다(웹훅 checkout.session.settled).
unresolved_underpaidLess than the amount arrived. The buyer can top up until the deadline.금액보다 적게 입금되었습니다. 구매자는 기한까지 부족분을 추가로 보낼 수 있습니다.Wait. If it is still short at the deadline, it is refunded and the session expires.기다립니다. 기한까지 부족하면 입금액은 환불되고 세션은 만료됩니다.
wrong_tokenA different token was sent. It is returned to the refund address; the session stays payable.다른 토큰이 입금되었습니다. 이 토큰은 환불 주소로 반환되며, 세션은 계속 결제할 수 있습니다.Nothing.없음.
expiredThe payment window closed without a full payment.결제 기한 안에 전액이 들어오지 않았습니다.Release the order. A full payment can still arrive later — see Late payments.주문을 해제합니다. 전액 결제가 나중에 도착할 수도 있습니다 — 아래 늦은 결제를 참고하세요.
Chain reorganisations. Rarely, a confirmed deposit disappears after a chain reorg. You then receive checkout.session.unresolved with reason: "reorg" and the session goes back to awaiting_deposit (or expired past the deadline). If you have already fulfilled, treat it like a reversed payment.
체인 재구성(reorg). 드물게 확정된 입금이 체인 reorg로 사라질 수 있습니다. 이때 reason: "reorg"가 담긴 checkout.session.unresolved를 받고, 세션은 awaiting_deposit(기한이 지났다면 expired)로 돌아갑니다. 이미 주문을 처리했다면 결제가 취소된 것으로 취급하세요.

Amounts금액

Field필드Unit단위
line_items[].amount (input)line_items[].amount (입력)Plain number string in the line item's currency: "29", "0.5". USDC/USDT = USD, JPYC = JPY. No FX conversion.line item currency 기준의 일반 숫자 문자열: "29", "0.5". USDC/USDT = USD, JPYC = JPY. 환율 변환은 없습니다.
amount_atomic (input), amount_totalamount_atomic (입력), amount_totalAlways a 6-decimal integer string, whatever token the buyer pays with: "29000000" = 29.구매자가 어떤 토큰으로 결제하든 항상 소수점 6자리 정수 문자열입니다: "29000000" = 29.
expectedAmount, pay_amount, amount_received, webhook amount_expected/amount_received, refund amountexpectedAmount, pay_amount, amount_received, 웹훅 amount_expected/amount_received, 환불 amountInteger string in the pay token's own decimals (token_decimals: 6 for USDC/USDT on most chains, 18 for BNB USDT and JPYC).결제 토큰 자체의 decimals 기준 정수 문자열(token_decimals: 대부분 체인의 USDC/USDT는 6, BNB USDT와 JPYC는 18).
fee_amount, net_amount6-decimal integer strings; fee_rate is a decimal fraction ("0.015" = 1.5%).소수점 6자리 정수 문자열이며, fee_rate는 소수 비율입니다("0.015" = 1.5%).

Handle all amounts as strings or big integers — never as floating-point numbers.

모든 금액은 문자열이나 큰 정수(BigInt 등)로 다루고, 부동소수점 숫자로 계산하지 마세요.

Minimum amount. A session's total (sum of amount × quantity) must be at least 1.00 in the session currency (USDC/USDT: $1, JPYC: ¥1) — a lower total returns 400 CHECKOUT_AMOUNT_TOO_LOW. The schema can't express this (it is a rule on the sum, and amount alone may be "0"), so check it before calling. Per-network minimums come on top: Tron currently $300 (testnet included) and JPYC ¥100 on every network. A network whose minimum is above the total is not offered to the buyer; requesting a deposit address on it returns 400 CHECKOUT_BELOW_MIN_PAYMENT.

최소 금액. 세션 총액(amount × quantity의 합)은 세션 통화 기준 1.00 이상이어야 합니다(USDC/USDT: $1, JPYC: ¥1). 이보다 적으면 400 CHECKOUT_AMOUNT_TOO_LOW가 반환됩니다. 합계에 대한 규칙이고 amount 하나는 "0"일 수도 있어서 스키마로는 표현되지 않으니, 호출 전에 직접 확인하세요. 네트워크별 최소 금액이 추가로 적용됩니다 — 현재 Tron은 $300(테스트넷 포함), JPYC는 모든 네트워크에서 ¥100입니다. 최소 금액이 총액보다 큰 네트워크는 구매자에게 제시되지 않으며, 그 네트워크로 입금 주소를 요청하면 400 CHECKOUT_BELOW_MIN_PAYMENT가 반환됩니다.

Session fields worth knowing알아 둘 세션 필드

  • deposit_tx_hash is the buyer's payment into the deposit address; sweep_tx_hash is the transaction that settled it to your payout wallet (or, for a refunded or rescued session, that on-chain action).
  • tx_hash is deprecated. It belonged to an earlier payment path and is kept only for compatibility — it now returns the same value as deposit_tx_hash. Use deposit_tx_hash / sweep_tx_hash.
  • received_token is the address of the token contract (or Solana mint) that actually arrived — not a symbol. received_token_symbol gives the symbol (USDC, USDT …), or null for a token BitPal doesn't know (for example a wrong token).
  • payer_wallet is the address the deposit came from (or the wallet the buyer connected), in the network's own format — Tron addresses are base58 T…. It may be an exchange's hot wallet, so don't use it as the buyer's identity or refund destination; refunds go to refund_address.
  • metadata is returned on the session as you sent it, and is also included in every webhook's data (see Webhooks → Event data). Keys BitPal writes itself — _bitpal, payer_email, allowed_pay_chains, allowed_assets — are reserved and are left out of both; don't use them for your own data.
  • deposit_tx_hash는 구매자가 입금 주소로 보낸 결제 트랜잭션이고, sweep_tx_hash는 그 자금을 정산 지갑으로 정산한 트랜잭션입니다(환불되거나 복구(rescue)된 세션이라면 해당 온체인 처리 트랜잭션).
  • tx_hash는 deprecated입니다. 예전 결제 경로에서 쓰던 필드로 호환성을 위해서만 남아 있으며, 지금은 deposit_tx_hash와 같은 값을 반환합니다. deposit_tx_hash / sweep_tx_hash를 사용하세요.
  • received_token은 실제로 입금된 토큰 컨트랙트(또는 Solana mint)의 주소이며, 심볼이 아닙니다. 심볼은 received_token_symbol(USDC, USDT …)에 있고, BitPal이 모르는 토큰(예: 잘못 보낸 토큰)이면 null입니다.
  • payer_wallet은 입금을 보낸 주소(또는 구매자가 연결한 지갑)이며 네트워크 고유 형식입니다 — Tron 주소는 base58 T…입니다. 거래소 핫월렛일 수 있으므로 구매자 식별이나 환불 대상으로 쓰지 마세요. 환불은 refund_address로 갑니다.
  • metadata는 세션 조회 시 보낸 그대로 반환되며, 모든 웹훅의 data에도 포함됩니다(웹훅 → 이벤트 데이터 참고). BitPal이 직접 쓰는 키 — _bitpal, payer_email, allowed_pay_chains, allowed_assets — 는 예약되어 있어 두 곳 모두에서 빠지니 자체 데이터에 쓰지 마세요.

Buyer experience구매자 경험

  • Receipts. BitPal does not email the buyer automatically. After paying, the buyer can request an emailed receipt on the payment completion screen; customer_email only pre-fills that form. If every buyer needs a receipt or order confirmation, send it yourself from your checkout.session.paid handler.
  • Status link. checkout_url (/pay/{id}?token=…) is also the buyer's status page — opening it later shows the payment's current state, and the receipt email links back to it. The token grants access to that page, so share the URL only with the buyer.
  • Language. The payment page picks its language from the buyer's browser (12 languages, English as fallback), and the buyer can switch it. You can't set the language from the session or the URL yet.
  • Your name and logo. The payment page does not show a merchant name or logo yet. Buyers see the line item names and amounts — make item names recognisable (for example include your store name).
  • Support. BitPal doesn't contact your buyers. Product, order and refund questions are yours to answer (completed payments are refunded by you — see Refunds above). For payment problems — funds sent but not detected, a wrong network, a late transfer — contact BitPal support with the session id and the transaction hash.
  • 영수증. BitPal은 구매자에게 자동으로 이메일을 보내지 않습니다. 구매자는 결제 완료 화면에서 영수증 이메일을 요청할 수 있고, customer_email은 그 입력란을 미리 채우는 용도일 뿐입니다. 모든 구매자에게 영수증이나 주문 확인이 필요하다면 checkout.session.paid 핸들러에서 직접 보내세요.
  • 상태 링크. checkout_url(/pay/{id}?token=…)은 구매자의 결제 상태 페이지이기도 합니다 — 나중에 열면 결제의 현재 상태가 보이고, 영수증 이메일도 이 페이지로 연결됩니다. token이 페이지 접근 권한이므로 URL은 구매자에게만 공유하세요.
  • 언어. 결제 페이지는 구매자의 브라우저 설정에 따라 언어를 고르며(12개 언어, 기본은 영어), 구매자가 직접 바꿀 수도 있습니다. 세션이나 URL로 언어를 지정하는 기능은 아직 없습니다.
  • 가맹점 이름과 로고. 결제 페이지에는 아직 가맹점 이름이나 로고가 표시되지 않습니다. 구매자는 line item의 name과 금액을 보게 되니, 품목 이름을 알아보기 쉽게 지으세요(예: 스토어 이름 포함).
  • 고객 지원. BitPal은 구매자에게 직접 연락하지 않습니다. 상품·주문·환불 문의는 가맹점이 응대합니다(완료된 결제의 환불은 가맹점이 직접 처리합니다 — 아래 환불 참고). 송금했는데 감지되지 않음, 잘못된 네트워크, 늦은 송금 같은 결제 문제는 세션 id와 트랜잭션 해시를 함께 BitPal 지원팀에 문의하세요.

Payment window and late payments결제 기한과 늦은 결제

  • The buyer has expires_in_minutes (1–60, default 15) from session creation to pay. That deadline is the session's expires_at.
  • late compares the block time of the deposit transaction with expires_at. A transfer sent in time is "late": false even if its confirmations finish after the deadline; checkout.session.settled carries the same value as paid.
  • Once a deposit address has been issued, BitPal keeps the session open for 90 minutes after expires_at so transfers already in flight can confirm. Underpayment refunds and checkout.session.expired for an issued session happen at the end of that window.
  • A full payment that arrives after the deadline is accepted as a late payment for up to 7 days by default: you get checkout.session.paid with "late": true and the funds settle normally. Decide whether to fulfil, or refund it from the console (see Refunds below).
  • Don't want late payments? Create the session with "late_payments": "refund". A full payment that arrives after the deadline (expires_at + 90 minutes) is then never accepted: it stays in the deposit address and is refunded automatically to the buyer's refund address about 7 days after the deadline (refund kind late_deposit, checkout.refund.recorded). The refund can't be instant — BitPal is non-custodial and the deposit contract only lets anyone but you take the funds back after that 7-day window. A transfer sent before expires_at whose confirmations finish later is still accepted.
  • This only applies to a session with no earlier on-chain action. If a wrong token was already returned or an underpayment already refunded, the expired session is closed for good: later deposits to its address are not accepted as payment (contact support with the session id and transaction hash).
  • Acceptance ends 1 hour before the on-chain refund window opens (expires_at + 90 minutes + 7 days). A full payment that arrives after that cutoff is not accepted; it is refunded automatically once the window opens (refund kind late_deposit).
  • A partial payment after the deadline is refunded automatically (refund kind late_deposit).
  • Funds sent more than 7 days after the deadline are not watched automatically. Contact support with the session id and transaction hash.
  • 구매자는 세션 생성 후 expires_in_minutes(1–60, 기본 15) 안에 결제해야 합니다. 이 기한이 세션의 expires_at입니다.
  • late는 입금 트랜잭션의 블록 시간을 expires_at과 비교해 정해집니다. 기한 안에 보낸 송금은 블록 확정이 기한 뒤에 끝나도 "late": false이며, checkout.session.settled에도 paid와 같은 값이 담깁니다.
  • 입금 주소가 발급된 세션은 이미 전송 중인 송금이 확정될 수 있도록 expires_at 후 90분 동안 열려 있습니다. 주소가 발급된 세션의 과소 입금 환불과 checkout.session.expired는 이 90분이 끝날 때 처리됩니다.
  • 기한 뒤에 도착한 전액 결제는 늦은 결제로 인정됩니다(기본값, 최대 7일). "late": true가 담긴 checkout.session.paid를 받고 자금은 정상적으로 정산됩니다. 주문을 처리할지, 콘솔에서 환불할지 판단하세요(아래 환불 참고).
  • 늦은 결제를 받지 않으려면 세션을 "late_payments": "refund"로 만드세요. 그러면 기한(expires_at + 90분) 뒤에 도착한 전액 결제는 결제로 인정되지 않고, 입금 주소에 머물러 있다가 기한 약 7일 뒤 구매자의 환불 주소로 자동 환불됩니다(환불 kind late_deposit, checkout.refund.recorded). 즉시 환불은 불가능합니다 — BitPal은 비수탁 방식이고, 입금 컨트랙트상 판매자가 아닌 쪽은 그 7일이 지나야 자금을 되돌릴 수 있습니다. expires_at 전에 보냈고 확정만 늦게 끝난 송금은 여전히 결제로 인정됩니다.
  • 이 규칙은 이전에 온체인 처리가 없었던 세션에만 적용됩니다. 잘못된 토큰을 이미 반환했거나 과소 입금을 이미 환불했다면, 만료된 세션은 완전히 닫힙니다. 이후 그 주소로 들어온 입금은 결제로 인정되지 않습니다(세션 id와 트랜잭션 해시를 함께 지원팀에 문의하세요).
  • 늦은 결제 인정은 온체인 환불 창이 열리기(expires_at + 90분 + 7일) 1시간 전에 끝납니다. 그 이후 도착한 전액 결제는 인정되지 않으며, 환불 창이 열리면 자동으로 환불됩니다(환불 kind late_deposit).
  • 기한 뒤에 들어온 일부 결제는 자동으로 환불됩니다(환불 kind late_deposit).
  • 기한 후 7일이 지나서 보낸 자금은 자동으로 감시되지 않습니다. 세션 id와 트랜잭션 해시를 함께 지원팀에 문의하세요.

Refunds환불

BitPal is non-custodial. A payment goes from the buyer to a one-time deposit address and then straight to your payout wallet — BitPal never holds it and has no right to pull it back. So there is no refund API. There are two kinds of refund, and both fire checkout.refund.recorded:

BitPal은 비수탁(non-custodial) 방식입니다. 결제 대금은 구매자에게서 일회용 입금 주소를 거쳐 곧바로 판매자 정산 지갑으로 갑니다 — BitPal은 자금을 보관하지 않으며 되돌려 가져올 권한도 없습니다. 그래서 환불 API는 없습니다. 환불은 두 종류이며, 둘 다 checkout.refund.recorded를 발생시킵니다.

1. Automatic refunds (mismatched payments)1. 자동 환불 (주문과 맞지 않는 결제)

Payments that don't match the order are returned on-chain from the deposit address to the buyer's refund address — automatically, with no action from you.

주문과 맞지 않는 결제는 입금 주소에서 구매자의 환불 주소로 온체인 반환됩니다 — 판매자가 할 일 없이 자동으로 처리됩니다.

kindWhen발생 조건Session outcome세션 결과
overpay_excessMore than the amount arrived.금액보다 많이 입금되었습니다.Paid; the exact amount settles, the excess is refunded.결제 완료. 정확한 금액이 정산되고 초과분은 환불됩니다.
underpay_expiredStill short at the deadline.기한까지 금액이 부족했습니다.Expired; everything received is refunded.만료. 받은 금액 전체가 환불됩니다.
wrong_tokenA token other than the selected one was sent.선택한 토큰이 아닌 다른 토큰이 입금되었습니다.Unchanged; still payable with the right token.변화 없음. 올바른 토큰으로 계속 결제할 수 있습니다.
late_depositA partial payment arrived after the deadline, or a full payment arrived after the late-acceptance cutoff (see above).기한 뒤에 일부 결제가 도착했거나, 늦은 결제 인정 마감 이후 전액 결제가 도착했습니다(위 참고).Stays expired.만료 상태가 유지됩니다.

2. Refunding a completed payment (from the console)2. 완료된 결제 환불 (콘솔에서)

Like an app store, a completed payment is refunded by you, in the BitPal console:

앱스토어처럼, 완료된 결제는 판매자가 BitPal 콘솔에서 직접 환불합니다.

  1. Open Console → Sessions → the session → Refund. It shows the network, token, the buyer's refund address (entered at payment time) and how much is still refundable (received minus refunds so far). Partial refunds are fine.
  2. Send the refund from your own wallet — one click with a connected EVM wallet, or copy the address, amount and token contract (with a QR code) for Tron, Solana or any other wallet.
  3. Record the transaction hash. BitPal checks it on-chain (confirmed, successful, the session's token sent to the refund address, after the payment, not more than what is left) and records it. You get checkout.refund.recorded with kind: "merchant", the reason_code you picked and from_address; GET /v1/checkout/sessions/{id}/refunds lists it.
  1. 콘솔 → 세션 → 해당 세션 → 환불을 엽니다. 네트워크, 토큰, 구매자의 환불 주소(결제 시 입력), 환불 가능한 잔액(받은 금액에서 지금까지의 환불액을 뺀 값)이 표시됩니다. 부분 환불도 가능합니다.
  2. 판매자 본인의 지갑에서 환불액을 보냅니다 — EVM 지갑을 연결했다면 클릭 한 번으로, Tron·Solana나 다른 지갑이라면 주소·금액·토큰 컨트랙트를 복사(QR 코드 제공)해서 보냅니다.
  3. 트랜잭션 해시를 기록합니다. BitPal이 온체인에서 확인한 뒤(확정·성공 여부, 세션 토큰이 환불 주소로 전송되었는지, 결제 이후의 거래인지, 남은 금액을 넘지 않는지) 기록합니다. kind: "merchant", 선택한 reason_code, from_address가 담긴 checkout.refund.recorded를 받게 되며, GET /v1/checkout/sessions/{id}/refunds로 조회할 수 있습니다.

Current policy: you pay the network fee and the refund amount; the BitPal fee on the original payment is not returned. Recording is console-only — an API key gets 403 REFUND_CONSOLE_ONLY. Recording the same transaction again returns the existing record.

현재 정책: 네트워크 수수료(가스비)와 환불액은 판매자가 부담하며, 원래 결제의 BitPal 수수료는 돌려주지 않습니다. 환불 기록은 콘솔에서만 할 수 있습니다 — API 키로 요청하면 403 REFUND_CONSOLE_ONLY가 반환됩니다. 같은 트랜잭션을 다시 기록하면 기존 기록이 반환됩니다.

Treat kind as open: new values may be added. Handle unknown kinds as a generic refund.

kind에는 새 값이 추가될 수 있습니다. 알 수 없는 kind는 일반 환불로 처리하세요.

Webhooks웹훅

Signed JSON events as a session moves through its lifecycle. Delivered at least once — keep handlers idempotent.

세션이 생애주기를 거칠 때마다 서명된 JSON 이벤트를 보냅니다. 최소 한 번(at-least-once) 전달되므로 핸들러는 멱등하게 만드세요.

Events이벤트

Event이벤트Fires when발생 시점
checkout.session.createdA session is created.세션이 생성되었습니다.
checkout.session.deposit_detectedFunds seen on-chain, awaiting confirmations. Optional — skipped when the deposit is already confirmed the first time BitPal sees it (common for a normal payment); paid then arrives on its own. Never wait for it.온체인에서 입금이 확인되어 블록 확정을 기다리는 중입니다. 선택적 이벤트입니다 — BitPal이 처음 확인했을 때 이미 확정된 입금이면(정상 결제에서 흔합니다) 생략되고 paid만 옵니다. 이 이벤트를 기다리지 마세요.
checkout.session.paidConfirmed — safe to fulfil. late: true if it arrived after the deadline.확정되었습니다 — 주문을 처리해도 안전합니다. 기한 뒤에 도착했다면 late: true입니다.
checkout.session.settledFunds swept to your payout wallet.자금이 정산 지갑으로 정산되었습니다.
checkout.session.unresolvedUnderpaid, wrong token, or a confirmed deposit reverted by a reorg (reason).과소 입금, 잘못된 토큰, 또는 확정된 입금이 reorg로 취소되었습니다(reason).
checkout.session.expiredDeadline passed without full payment.기한까지 전액이 결제되지 않았습니다.
checkout.refund.recordedA refund was made on-chain (kind): automatic, or one you sent and recorded in the console (merchant).온체인 환불이 이루어졌습니다(kind): 자동 환불이거나, 가맹점이 직접 보내고 콘솔에서 기록한 환불(merchant)입니다.
x402.payment.resolvedAn x402 call that settled with 503 reached its outcome. Only fires for those — the normal path answers inline.정산 결과가 503이었던 x402 호출의 결과가 확정되었습니다. 그런 호출에만 발생합니다 — 정상 경로에서는 응답으로 바로 결과를 알려 줍니다.

Verify & handle검증 & 처리

Each request carries X-Webhook-Signature-256 (sha256=<hex>) and X-Webhook-Timestamp. Verify both, against the raw bytes.

모든 요청에는 X-Webhook-Signature-256(sha256=<hex>)과 X-Webhook-Timestamp가 담깁니다. 두 값 모두 원본(raw) 바이트를 기준으로 검증하세요.

import express from 'express';
import { verifyWebhookSignature, parseWebhookEvent, WEBHOOK_HEADERS } from '@bitpal/checkout';

app.post('/webhooks/bitpal', express.raw({ type: 'application/json' }), async (req, res) => {
  const raw = req.body.toString('utf8');            // exact bytes, not re-serialized JSON
  const ok = await verifyWebhookSignature(           // returns a Promise — always await it
    raw,
    req.header(WEBHOOK_HEADERS.signature) ?? '',
    process.env.BITPAL_WEBHOOK_SECRET,
    { timestamp: req.header(WEBHOOK_HEADERS.timestamp) ?? '' }, // replay defense — required
  );
  if (!ok) return res.status(400).send('invalid signature');
  const event = parseWebhookEvent(raw);
  if (event.event === 'checkout.session.paid') await fulfil(event.data.session_id); // idempotent, dedup on event.id
  res.sendStatus(200);
});
Verify against the raw body, and await the result. Re-serializing the JSON changes the bytes and breaks the signature — capture the raw body before JSON middleware. verifyWebhookSignature is async: without await the check is always truthy and forged requests pass.원본(raw) 본문으로 검증하고, 결과를 await 하세요. JSON을 다시 직렬화하면 바이트가 바뀌어 서명 검증이 실패합니다 — JSON 미들웨어보다 먼저 원본 본문을 확보하세요. verifyWebhookSignature는 async라서 await 없이 쓰면 항상 참으로 판정되어 위조 요청이 통과합니다.

Request format요청 형식

Every delivery is an HTTP POST with a JSON body:

모든 전달은 JSON 본문을 담은 HTTP POST입니다.

{
  "id": "1ff63374-9417-4f87-850d-e869a3bd2dbe",   // event id — dedup on this
  "event": "checkout.session.paid",
  "created_at": "2026-11-06T07:20:49.549Z",       // when the event happened (unchanged on retries)
  "data": { "session_id": "cs_…", … }
}
Header헤더Value값
X-Webhook-Signature-256sha256=<hex> — HMAC-SHA256 of <X-Webhook-Timestamp>.<raw body>sha256=<hex> — <X-Webhook-Timestamp>.<raw body>의 HMAC-SHA256
X-Webhook-TimestampISO-8601 UTC time of this attempt (new on every retry)이번 시도의 ISO-8601 UTC 시각(재시도마다 새로 설정)
X-Webhook-EventEvent name (unsigned — read it from the body)이벤트 이름(서명 대상 아님 — 본문에서 읽으세요)
X-Webhook-IdEvent id (unsigned — dedup on the body id)이벤트 id(서명 대상 아님 — 본문의 id로 중복을 제거하세요)
X-Webhook-Delivery-IdId of this delivery, for support requests이번 전달의 id, 지원 문의용

Verifying the signature서명 검증

  1. Read the raw request body as bytes, before any JSON parsing.
  2. Reject the request if X-Webhook-Timestamp is more than 5 minutes from your clock (replay protection).
  3. Compute HMAC-SHA256(key = your whsec_… secret, message = timestamp + "." + raw body) as lowercase hex.
  4. Compare it with the part after sha256= using a constant-time comparison.
  1. JSON을 파싱하기 전에 원본 요청 본문을 바이트로 읽습니다.
  2. X-Webhook-Timestamp가 서버 시계와 5분 넘게 차이 나면 요청을 거부합니다(재전송 공격 방지).
  3. HMAC-SHA256(key = your whsec_… secret, message = timestamp + "." + raw body)를 소문자 hex로 계산합니다.
  4. sha256= 뒤의 값과 상수 시간(constant-time) 비교로 대조합니다.

Python

import hmac, hashlib, time
from datetime import datetime

def verify_bitpal_webhook(raw_body: bytes, signature: str, timestamp: str, secret: str, tolerance_sec: int = 300) -> bool:
    if not signature.startswith("sha256=") or not timestamp:
        return False
    try:
        sent = datetime.fromisoformat(timestamp.replace("Z", "+00:00")).timestamp()
    except (ValueError, OverflowError, OSError):
        return False
    if abs(time.time() - sent) > tolerance_sec:
        return False
    expected = hmac.new(secret.encode(), timestamp.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
    # compare bytes: compare_digest raises TypeError on non-ASCII str input
    return hmac.compare_digest(signature[len("sha256="):].encode(), expected.encode())

# Flask: verify_bitpal_webhook(request.get_data(), request.headers.get("X-Webhook-Signature-256", ""),
#                              request.headers.get("X-Webhook-Timestamp", ""), os.environ["BITPAL_WEBHOOK_SECRET"])

Go

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"strings"
	"time"
)

func VerifyBitPalWebhook(rawBody []byte, signature, timestamp, secret string) bool {
	const prefix = "sha256="
	if !strings.HasPrefix(signature, prefix) || timestamp == "" {
		return false
	}
	sent, err := time.Parse(time.RFC3339Nano, timestamp)
	if err != nil {
		return false
	}
	if d := time.Since(sent); d > 5*time.Minute || d < -5*time.Minute {
		return false
	}
	mac := hmac.New(sha256.New, []byte(secret))
	mac.Write([]byte(timestamp + "."))
	mac.Write(rawBody)
	expected := hex.EncodeToString(mac.Sum(nil))
	return hmac.Equal([]byte(signature[len(prefix):]), []byte(expected))
}

PHP

function verify_bitpal_webhook(string $rawBody, string $signature, string $timestamp, string $secret, int $toleranceSec = 300): bool {
    if (strpos($signature, 'sha256=') !== 0 || $timestamp === '') return false;
    $sent = strtotime($timestamp);
    if ($sent === false || abs(time() - $sent) > $toleranceSec) return false;
    $expected = hash_hmac('sha256', $timestamp . '.' . $rawBody, $secret);
    return hash_equals($expected, substr($signature, 7));
}

$secret = getenv('BITPAL_WEBHOOK_SECRET');   // false when unset
if ($secret === false || $secret === '') {
    http_response_code(500);                  // misconfigured — never verify against an empty key
    exit;
}
$ok = verify_bitpal_webhook(
    (string) file_get_contents('php://input'),
    $_SERVER['HTTP_X_WEBHOOK_SIGNATURE_256'] ?? '',
    $_SERVER['HTTP_X_WEBHOOK_TIMESTAMP'] ?? '',
    $secret
);

Java

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.time.DateTimeException;
import java.time.Duration;
import java.time.Instant;

public static boolean verifyBitPalWebhook(byte[] rawBody, String signature, String timestamp, String secret) throws Exception {
    if (signature == null || !signature.startsWith("sha256=") || timestamp == null || timestamp.isEmpty()) return false;
    Instant sent;
    try { sent = Instant.parse(timestamp); } catch (DateTimeException e) { return false; }
    if (Duration.between(sent, Instant.now()).abs().getSeconds() > 300) return false;
    Mac mac = Mac.getInstance("HmacSHA256");
    mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
    mac.update((timestamp + ".").getBytes(StandardCharsets.UTF_8));
    byte[] digest = mac.doFinal(rawBody);
    StringBuilder hex = new StringBuilder();
    for (byte b : digest) hex.append(String.format("%02x", b));
    return MessageDigest.isEqual(hex.toString().getBytes(StandardCharsets.UTF_8),
                                 signature.substring(7).getBytes(StandardCharsets.UTF_8));
}

Event data이벤트 데이터

Every data object has session_id and metadata — the metadata you set when creating the session ({} if none), so you can map an event to your order without a lookup. BitPal's reserved keys (_bitpal, payer_email, allowed_pay_chains, allowed_assets) are left out. Session events also carry amount_total (6-decimal integer string), currency and late. Token amounts are in the pay token's decimals — see Payment lifecycle → Amounts.

모든 data 객체에는 session_id와 metadata가 있습니다. metadata는 세션을 만들 때 넣은 값이며(없으면 {}), 별도 조회 없이 이벤트를 주문과 연결할 수 있습니다. BitPal 예약 키(_bitpal, payer_email, allowed_pay_chains, allowed_assets)는 제외됩니다. 세션 이벤트에는 amount_total(소수점 6자리 정수 문자열), currency, late도 담깁니다. 토큰 금액은 결제 토큰의 decimals 기준입니다 — 결제 생애주기 → 금액을 참고하세요.

Event이벤트Extra fields in datadata의 추가 필드
checkout.session.created— (only session_id, amount_total, currency)— (session_id, amount_total, currency만)
checkout.session.deposit_detectedtx_hash, amount_received, received_token (token contract) — optional event, see abovetx_hash, amount_received, received_token(토큰 컨트랙트) — 선택적 이벤트, 위 참고
checkout.session.paidtx_hash, late
checkout.session.settledsweep_tx_hash
checkout.session.unresolvedreason: underpaid (+ amount_received, amount_expected, received_token, tx_hash) · wrong_token (+ received_token) · reorg
checkout.session.expiredreason: "no_deposit", or refund_tx_hash when a partial payment was refundedreason: "no_deposit", 또는 일부 결제가 환불된 경우 refund_tx_hash
checkout.refund.recordedkind (overpay_excess · underpay_expired · wrong_token · late_deposit · merchant), amount, token, tx_hash, refund_address; for merchant also from_address and reason_codekind(overpay_excess · underpay_expired · wrong_token · late_deposit · merchant), amount, token, tx_hash, refund_address. merchant인 경우 from_address와 reason_code도 포함

Example: checkout.session.paid예시: checkout.session.paid

{
  "id": "1ff63374-9417-4f87-850d-e869a3bd2dbe",
  "event": "checkout.session.paid",
  "created_at": "2026-11-06T07:20:49.549Z",
  "data": {
    "session_id": "cs_HFoj91aFFbHSRk1LN1-9K01q",
    "amount_total": "1000000",
    "currency": "USDC",
    "late": false,
    "tx_hash": "0xd9a75dfe753b20027a0a93841c4e70af1b6be061b59e530d1bc381e88c196fe3",
    "metadata": { "order_id": "ord_1042" }
  }
}

Example: checkout.session.unresolved (underpaid)예시: checkout.session.unresolved (과소 입금)

{
  "id": "18d80275-45be-43d7-a57b-4b5038bd2736",
  "event": "checkout.session.unresolved",
  "created_at": "2026-11-06T07:14:19.252Z",
  "data": {
    "session_id": "cs_1ATJ7V5QknzeFpTNPMU_449s",
    "amount_total": "5000000",
    "currency": "USDC",
    "late": false,
    "reason": "underpaid",
    "amount_expected": "5000000",
    "amount_received": "2500000",
    "received_token": "0x943b94Dacf1507F2658f1CfE103Ac36Ebc13Ef65",
    "tx_hash": "0x4461291b5d743d3950a474c015098b8cb53b4373346bd510f74481dd8dbb9d65",
    "metadata": { "order_id": "ord_1041" }
  }
}

Example: checkout.refund.recorded예시: checkout.refund.recorded

{
  "id": "f2fdcce6-1880-44b2-8351-906c42b7c4cd",
  "event": "checkout.refund.recorded",
  "created_at": "2026-11-06T07:20:29.547Z",
  "data": {
    "session_id": "cs_YsrZGcZzVsVHJo8ZNVmsTL1D",
    "kind": "overpay_excess",
    "amount": "1000000",
    "token": "0x943b94Dacf1507F2658f1CfE103Ac36Ebc13Ef65",
    "tx_hash": "0xe943412e2be8b3e3fc6359b47a8eea0cc4de17768b069f52ebab1aacd873aec4",
    "refund_address": "0xa0Ee7A142d267C1f36714E4a8F75612F20a79720",
    "metadata": { "order_id": "ord_1040" }
  }
}

Delivery, retries and ordering전달, 재시도, 순서

  • Respond with any 2xx within 10 seconds. Do slow work after responding (queue it).
  • A failed delivery (non-2xx, timeout or connection error) is retried right away twice (after 1 s and 2 s), then after 1 min, 5 min, 30 min, 2 h, 6 h, 12 h and 24 h — 10 attempts over about a day. After the last one the delivery moves to the dead-letter queue; resend it from Console → Developers → Webhooks once your endpoint is fixed (dead-lettered deliveries can be resent for 30 days).
  • Delivery is at least once: the same event can arrive more than once. Deduplicate on the body id and store it in the same transaction as your side effect.
  • Order is not guaranteed — a retried paid can arrive after settled. Keep the furthest state you have seen instead of overwriting it with an older one. Not every event is sent for every payment: paid can arrive with no deposit_detected before it (a late payment, or a deposit already confirmed when it was first seen).
  • Test and live are separate: each environment has its own endpoints and secrets.
  • 10초 안에 2xx로 응답하세요. 오래 걸리는 작업은 응답한 뒤에 처리하세요(큐 사용).
  • 전달에 실패하면(2xx 외 응답, 타임아웃, 연결 오류) 바로 두 번(1초, 2초 후) 재시도하고, 이후 1분, 5분, 30분, 2시간, 6시간, 12시간, 24시간 뒤에 재시도합니다 — 약 하루 동안 총 10회입니다. 마지막 시도까지 실패하면 dead-letter queue로 옮겨지며, 엔드포인트를 고친 뒤 콘솔 → 개발자 → 웹훅에서 다시 보낼 수 있습니다(dead-letter 전달은 30일 동안 재전송 가능).
  • 전달은 최소 한 번(at-least-once) 보장입니다. 같은 이벤트가 여러 번 올 수 있으니 본문의 id로 중복을 제거하고, 그 id를 부수 효과와 같은 트랜잭션에서 저장하세요.
  • 순서는 보장되지 않습니다 — 재시도된 paid가 settled보다 늦게 올 수 있습니다. 더 오래된 상태로 덮어쓰지 말고, 지금까지 본 가장 앞선 상태를 유지하세요. 모든 결제에 모든 이벤트가 오는 것도 아닙니다. paid 전에 deposit_detected가 없을 수 있습니다(늦은 결제이거나, 처음 확인했을 때 이미 확정된 입금).
  • 테스트와 라이브는 분리되어 있습니다. 환경마다 엔드포인트와 시크릿이 따로 있습니다.

Testing테스트

  • Send Test (Console → Developers → Webhooks) posts a signed sample event to your endpoint. Its id starts with test_.
  • In the Sandbox event list (and the MCP list_test_events tool) every event is a test-mode event. The is_test field does not mean test mode: it is true only for Send Test samples and false for events produced by a (simulated) payment.
  • For local development, expose your server with a tunnel (ngrok, cloudflared) and register the tunnel URL in test mode.
  • Run a real test payment (Quickstart) to receive the full sequence of events.
  • 테스트 전송(콘솔 → 개발자 → 웹훅)은 서명된 샘플 이벤트를 엔드포인트로 보냅니다. 이 이벤트의 id는 test_로 시작합니다.
  • 샌드박스 이벤트 목록(과 MCP list_test_events 도구)에 보이는 이벤트는 모두 테스트 모드 이벤트입니다. is_test 필드는 테스트 모드를 뜻하지 않습니다. 테스트 전송 샘플일 때만 true이고, (시뮬레이션) 결제로 발생한 이벤트는 false입니다.
  • 로컬 개발 중에는 터널(ngrok, cloudflared)로 서버를 노출하고, 그 터널 URL을 테스트 모드에 등록하세요.
  • 실제 테스트 결제(빠른 시작)를 진행하면 전체 이벤트 흐름을 받아 볼 수 있습니다.

Errors에러

How the API reports failures, and what to do about the ones you are likely to see.

API가 실패를 어떻게 알려 주는지, 그리고 자주 마주칠 에러에 어떻게 대응하는지 설명합니다.

Response format응답 형식

Successful responses are { "data": … }. Failed responses use an HTTP status code and this JSON body:

성공 응답은 { "data": … } 형식입니다. 실패 응답은 HTTP 상태 코드와 함께 다음 JSON 본문을 반환합니다.

{
  "error": "checkout not enabled for USDC on eip155:8453",   // human-readable
  "code": "FEATURE_NOT_ENABLED",                                // machine-readable
  "retryable": false,
  "trace_id": "4f1c…"                                           // quote it to support
}

error is a human-readable message and may change — branch on code (after the HTTP status), never on the text of error. Validation failures add a details array. The SDK exposes the same fields:

error는 사람이 읽기 위한 메시지이며 바뀔 수 있습니다 — error 문구가 아니라 (HTTP 상태 다음으로) code로 분기하세요. 유효성 검사 실패에는 details 배열이 추가됩니다. SDK도 같은 필드를 제공합니다.

import { BitPalError } from '@bitpal/checkout';

try {
  await bitpal.checkout.createSession(params);
} catch (err) {
  if (err instanceof BitPalError) {
    err.statusCode;   // 400, 404, 429, …
    err.code;         // 'CHECKOUT_AMOUNT_TOO_LOW' (when the API sent one)
    err.apiMessage;   // the API's description
    err.retryable;    // true / false / undefined
    err.traceId;      // for support
  }
  throw err;
}

HTTP status codesHTTP 상태 코드

Status상태Meaning의미Retry?재시도?
400The request is invalid — fix it.요청이 잘못되었습니다 — 요청을 수정하세요.No아니요
401Missing, invalid, revoked or expired API key, or a key for the other mode.API 키가 없거나, 유효하지 않거나, 폐기·만료되었거나, 다른 모드의 키입니다.No아니요
403Not allowed — for example the merchant account is frozen or suspended.허용되지 않는 요청입니다 — 예: 가맹점 계정이 동결되었거나 정지된 경우.No아니요
404The object doesn't exist in this environment (test and live are separate).이 환경에 해당 객체가 없습니다(테스트와 라이브는 분리되어 있습니다).No아니요
409Conflicts with the current state, or the same Idempotency-Key is still in progress.현재 상태와 충돌하거나, 같은 Idempotency-Key의 요청이 아직 처리 중입니다.After re-reading state상태를 다시 읽은 뒤
410Gone — the session or link has expired, sold out or been deactivated.더 이상 사용할 수 없습니다 — 세션이나 링크가 만료·매진·비활성화되었습니다.No아니요
422Idempotency-Key reused with a different body or on a different endpoint.Idempotency-Key를 다른 본문이나 다른 엔드포인트에 재사용했습니다.No — use a new key아니요 — 새 키를 사용하세요
429Rate limited. Honour Retry-After (seconds).요청 한도를 초과했습니다. Retry-After(초)를 따르세요.Yes, with backoff예, 백오프 적용
5xxBitPal-side problem.BitPal 측 문제입니다.Yes, with backoff (same Idempotency-Key)예, 백오프 적용(같은 Idempotency-Key로)

Common error codes자주 보는 에러 코드

Code코드HTTPCause → fix원인 → 해결
UNAUTHORIZED401Bad, missing, revoked or expired Authorization: Bearer bp_… key.Authorization: Bearer bp_… 키가 잘못되었거나, 없거나, 폐기·만료되었습니다.
VALIDATION_FAILED400Body doesn't match the schema — see details. Typical: a line item with both or neither of amount / amount_atomic.본문이 스키마와 맞지 않습니다 — details를 확인하세요. 흔한 예: line item에 amount / amount_atomic을 둘 다 넣었거나 둘 다 빠뜨린 경우.
MIXED_LINE_ITEMS400Raw line items and price_id items in one session. Use one kind.일반 line item과 price_id 항목을 한 세션에 섞었습니다. 한 종류만 사용하세요.
CURRENCY_MIX_NOT_ALLOWED400Line items in different currencies. Use one currency per session.line item의 통화가 서로 다릅니다. 세션 하나에는 통화 하나만 사용하세요.
CHECKOUT_AMOUNT_TOO_LOW400The total (sum of amount × quantity) is below 1.00 in the session currency ($1 for USDC/USDT, ¥1 for JPYC).총액(amount × quantity의 합)이 세션 통화 기준 1.00 미만입니다(USDC/USDT는 $1, JPYC는 ¥1).
UNSUPPORTED_CHAIN · UNSUPPORTED_ASSET · UNSUPPORTED_TOKEN · UNSUPPORTED_CURRENCY400Unknown chain/token, or a chain not enabled for checkout. See Chains & contracts.알 수 없는 체인·토큰이거나, 결제가 활성화되지 않은 체인입니다. 체인 & 컨트랙트를 참고하세요.
CHAIN_MODE_MISMATCH400A mainnet CAIP-2 id with a test key, or a testnet id with a live key. Use the matching id, or a short name (base) that resolves to your key's mode.테스트 키에 메인넷 CAIP-2 id를, 또는 라이브 키에 테스트넷 id를 썼습니다. 키에 맞는 id를 쓰거나, 키 모드에 맞게 해석되는 short name(base)을 쓰세요.
ALLOWED_CHAINS_CONFLICT400pay_chain and allowed_pay_chains were both set. Send one.pay_chain과 allowed_pay_chains를 함께 지정했습니다. 하나만 보내세요.
FEATURE_NOT_ENABLED400That asset is not enabled for checkout on that chain.해당 체인에서 그 자산은 결제가 활성화되어 있지 않습니다.
SELLER_WALLET_NOT_CONFIGURED400No payout wallet for that network family (EVM / Tron / Solana). Add one in Console → Settings → Payout Wallet.해당 네트워크 계열(EVM / Tron / Solana)의 정산 지갑이 없습니다. 콘솔 → 설정 → 정산 지갑에서 등록하세요.
INVALID_REFUND_ADDRESS400The refund address is not valid for the chosen network.선택한 네트워크에 맞지 않는 환불 주소입니다.
ASSET_SELECTION_REQUIRED400Multi-option session: pass chain and token when issuing the deposit address.결제 옵션이 여러 개인 세션입니다. 입금 주소를 발급할 때 chain과 token을 전달하세요.
ASSET_NOT_OFFERED400The chain/token you passed is not one of the session's payment_options.전달한 chain/token이 세션의 payment_options에 없습니다.
CHECKOUT_BELOW_MIN_PAYMENT400The amount is below the minimum payment for the chosen network (some networks, such as Tron, have a higher floor). Pick another option.금액이 선택한 네트워크의 최소 결제 금액보다 작습니다(Tron 등 일부 네트워크는 최소 금액이 더 높습니다). 다른 옵션을 고르세요.
SESSION_NOT_FOUND404Wrong id, or the session belongs to the other environment.id가 잘못되었거나, 다른 환경(테스트/라이브)의 세션입니다.
REFUND_CONSOLE_ONLY403Recording a refund is console-only — there is no refund API. Refund in Console → Sessions → session → Refund.환불 기록은 콘솔에서만 할 수 있습니다 — 환불 API는 없습니다. 콘솔 → 세션 → 해당 세션 → 환불에서 처리하세요.
PRODUCT_NOT_FOUND · PAYMENT_LINK_NOT_FOUND404Wrong id or environment.id나 환경이 잘못되었습니다.
PRICE_NOT_FOUND404 / 400404 when you fetch a price by id; 400 when a price_id in a session's line items or a payment link doesn't exist (wrong id or environment).id로 가격을 조회할 때는 404, 세션 line item이나 결제 링크의 price_id가 존재하지 않을 때는 400입니다(id나 환경 오류).
INVALID_STATE_TRANSITION409The session can't do that in its current state — for example issuing a deposit address for a session that is already paid or expired. Re-read the session; create a new one if it has expired.세션의 현재 상태에서는 할 수 없는 작업입니다 — 예: 이미 결제되었거나 만료된 세션에 입금 주소를 발급하려는 경우. 세션을 다시 조회하고, 만료되었다면 새로 만드세요.
CHECKOUT_SESSION_EXPIRED410A deposit address was requested after expires_at for a session that never got one. Create a new session.입금 주소를 한 번도 발급받지 않은 세션에 expires_at 이후 입금 주소를 요청했습니다. 새 세션을 만드세요.
LINK_EXPIRED · LINK_INACTIVE · LINK_SOLD_OUT410The payment link can't take payments any more.이 결제 링크로는 더 이상 결제를 받을 수 없습니다.
MERCHANT_FROZEN · MERCHANT_SUSPENDED403The account is restricted. Contact support.계정이 제한된 상태입니다. 지원팀에 문의하세요.
TOO_MANY_ACTIVE_SESSIONS429Over 1,000 open sessions. Let unused ones expire, and don't create a session before the buyer intends to pay.열려 있는 세션이 1,000개를 넘었습니다. 쓰지 않는 세션은 만료되게 두고, 구매자가 결제하려 할 때만 세션을 만드세요.
RATE_LIMIT_EXCEEDED429Too many requests. Wait Retry-After seconds.요청이 너무 많습니다. Retry-After초만큼 기다리세요.
INVALID_IDEMPOTENCY_KEY400Idempotency-Key must be 8–128 characters.Idempotency-Key는 8–128자여야 합니다.
IDEMPOTENCY_KEY_REUSED422Same key with a different body, or on a different endpoint (method and path are part of the fingerprint). Use a new key for a new request.같은 키를 다른 본문이나 다른 엔드포인트에 썼습니다(메서드와 경로도 fingerprint에 포함됩니다). 새 요청에는 새 키를 쓰세요.
IDEMPOTENCY_IN_PROGRESS409The first request with this key is still running. Retry shortly.이 키로 보낸 첫 요청이 아직 처리 중입니다. 잠시 후 재시도하세요.
INTERNAL_ERROR500Retry with backoff, with the same Idempotency-Key; if it persists, contact support with trace_id.같은 Idempotency-Key로 백오프를 두고 재시도하세요. 계속되면 trace_id와 함께 지원팀에 문의하세요.

Retrying safely안전하게 재시도하기

  • Send an Idempotency-Key (8–128 characters, unique per operation — an order id works) on any merchant POST (sessions, products, prices, payment links, webhook endpoints, …) — it is honoured on all of them. Replays within 24 hours return the original response with X-Idempotency-Replayed: true. A key is bound to one request: reusing it with a different body or on a different endpoint returns 422 IDEMPOTENCY_KEY_REUSED. Failed requests (4xx/5xx) are not cached, so you can retry them with the same key.
  • Retry only 429, 5xx and network errors, with exponential backoff.
  • Rate-limited responses carry Retry-After and X-RateLimit-Limit / -Remaining / -Reset.
  • 가맹점 POST 요청(세션, 상품, 가격, 결제 링크, 웹훅 엔드포인트 등)에는 Idempotency-Key(8–128자, 작업마다 고유 — 주문 id면 충분합니다)를 보내세요. 모든 POST에서 지원됩니다. 24시간 안의 재요청은 원래 응답을 X-Idempotency-Replayed: true와 함께 반환합니다. 키는 요청 하나에 묶입니다. 다른 본문이나 다른 엔드포인트에 재사용하면 422 IDEMPOTENCY_KEY_REUSED가 반환됩니다. 실패한 요청(4xx/5xx)은 캐시되지 않으므로 같은 키로 재시도할 수 있습니다.
  • 429, 5xx, 네트워크 오류만 지수 백오프로 재시도하세요.
  • 요청 한도에 걸린 응답에는 Retry-After와 X-RateLimit-Limit / -Remaining / -Reset 헤더가 담깁니다.

Use with AIAI와 함께 쓰기

Give your AI coding tool BitPal's docs, API spec and webhook schemas so it writes integrations against the real API — and, with a test key, runs a test payment for you.

AI 코딩 도구에 BitPal 문서, API 스펙, 웹훅 스키마를 제공해 실제 API에 맞는 연동 코드를 작성하게 하세요. 테스트 키를 주면 테스트 결제까지 대신 실행합니다.

BitPal MCP serverBitPal MCP 서버

  • Remote: https://mcp.bitpal.io/mcp (Streamable HTTP). Nothing to install.
  • Local: npx -y @bitpal/mcp (stdio).
  • Docs tools work without a key. Add a bp_test_… key to unlock test-mode tools. Live keys are rejected. The server checks the key with BitPal: a revoked or unknown key keeps the test-mode tools hidden and says why in the server instructions.
  • 원격: https://mcp.bitpal.io/mcp (Streamable HTTP). 설치할 것이 없습니다.
  • 로컬: npx -y @bitpal/mcp (stdio).
  • 문서 도구는 키 없이 동작합니다. bp_test_… 키를 추가하면 테스트 모드 도구가 열립니다. 라이브 키는 거부됩니다. 서버는 BitPal에 키를 확인하며, 폐기되었거나 알 수 없는 키라면 테스트 모드 도구를 숨긴 채 그 이유를 서버 instructions에 알려 줍니다.
Tool도구What it does기능
get_integration_guideIntegration rules plus the docs for one flow: hosted, popup, headless, payment_link, x402, webhooks.연동 규칙과 흐름 하나의 문서: hosted, popup, headless, payment_link, x402, webhooks.
search_docs · get_docSearch the docs; read a page as Markdown.문서를 검색하고, 페이지를 Markdown으로 읽습니다.
list_api_operations · get_api_operationREST operations with request/response schemas, from the OpenAPI spec.OpenAPI 스펙 기반 REST 작업과 요청/응답 스키마.
get_webhook_event · get_error_codeWebhook payload schemas; what an error code means and how to fix it.웹훅 payload 스키마, 에러 코드의 의미와 해결 방법.
create_test_session · simulate_payment · get_session_status · list_test_eventsTest key only. Create a test session, send the deposit (exact, underpay, overpay, wrong_token) and read the webhook events — the same as Console → Developers → Sandbox.테스트 키 전용. 테스트 세션을 만들고, 입금을 보내고(exact, underpay, overpay, wrong_token), 웹훅 이벤트를 읽습니다 — 콘솔 → 개발자 → 샌드박스와 같습니다.

Setup설정

Replace bp_test_… with your test key (Console → Developers → API Keys), or leave the key out for docs only.

bp_test_…를 본인의 테스트 키(콘솔 → 개발자 → API 키)로 바꾸세요. 문서만 쓸 거라면 키를 빼도 됩니다.

Restart your AI client after adding the server. MCP clients load their tool list when a session starts. In Claude Code, start a new session after claude mcp add (check with /mcp); in Cursor, VS Code, Windsurf, Claude Desktop and Codex, restart the app or reload the window. Do the same after changing the key.
서버를 추가한 뒤 AI 클라이언트를 다시 시작하세요. MCP 클라이언트는 세션이 시작될 때 도구 목록을 불러옵니다. Claude Code에서는 claude mcp add 후 새 세션을 시작하고(/mcp로 확인), Cursor·VS Code·Windsurf·Claude Desktop·Codex에서는 앱을 재시작하거나 창을 다시 로드하세요. 키를 바꾼 뒤에도 마찬가지입니다.

Claude Code

# remote
claude mcp add --transport http bitpal https://mcp.bitpal.io/mcp --header "Authorization: Bearer bp_test_…"
# local
claude mcp add bitpal -e BITPAL_API_KEY=bp_test_… -- npx -y @bitpal/mcp

Cursor — ~/.cursor/mcp.json

{
  "mcpServers": {
    "bitpal": { "url": "https://mcp.bitpal.io/mcp", "headers": { "Authorization": "Bearer bp_test_…" } }
  }
}

VS Code — .vscode/mcp.json

{
  "servers": {
    "bitpal": { "type": "http", "url": "https://mcp.bitpal.io/mcp", "headers": { "Authorization": "Bearer bp_test_…" } }
  }
}

Windsurf — ~/.codeium/windsurf/mcp_config.json

{
  "mcpServers": {
    "bitpal": { "serverUrl": "https://mcp.bitpal.io/mcp", "headers": { "Authorization": "Bearer bp_test_…" } }
  }
}

Claude Desktop — claude_desktop_config.json

{
  "mcpServers": {
    "bitpal": { "command": "npx", "args": ["-y", "@bitpal/mcp"], "env": { "BITPAL_API_KEY": "bp_test_…" } }
  }
}

Codex CLI — ~/.codex/config.toml

[mcp_servers.bitpal]
command = "npx"
args = ["-y", "@bitpal/mcp"]
env = { BITPAL_API_KEY = "bp_test_…" }

Without MCPMCP 없이 쓰기

  • llms.txt — an index of every page plus the rules below.
  • llms-full.txt — all docs in one file.
  • Every page has Copy as Markdown and a .md version (/docs/md/<page>.md).
  • openapi.json — the OpenAPI 3.1 spec, generated from the API code.
  • llms.txt — 모든 페이지의 색인과 아래 규칙.
  • llms-full.txt — 전체 문서를 파일 하나로.
  • 모든 페이지에 Copy as Markdown 버튼과 .md 버전(/docs/md/<page>.md)이 있습니다.
  • openapi.json — API 코드에서 생성한 OpenAPI 3.1 스펙.

Rules for your AGENTS.mdAGENTS.md에 넣을 규칙

Paste this into AGENTS.md, CLAUDE.md or your editor's rules file so every session follows it.

아래 내용을 AGENTS.md, CLAUDE.md 또는 에디터의 규칙 파일에 붙여 넣으면 모든 세션이 이 규칙을 따릅니다. (AI 도구용 규칙이라 영어로 제공합니다.)

## BitPal integration rules
- Keep bp_test_/bp_live_ API keys and the whsec_ webhook secret on the server. Browsers only get checkout_url.
- Fulfil orders from the checkout.session.paid webhook (signature verified) or a server-side session lookup —
  never from the success_url redirect or a popup message.
- Verify webhooks on the raw body with X-Webhook-Signature-256 + X-Webhook-Timestamp; await verifyWebhookSignature.
- Webhooks are at-least-once and unordered: dedupe on the body id, keep the furthest session state.
- Amounts are strings: amount = plain number in the line-item currency ("29"); amount_total = 6-decimal integer;
  deposit amounts use the token's decimals. No floating-point math.
- Paid statuses are paid_confirmed and swept (there is no "paid"/"settled" status value).
- Payment window: expires_in_minutes (1–60). Full payments up to 7 days late arrive with late: true — handle them.
- Test keys take testnet chain ids only; live keys mainnet only.
- Headless UIs call BitPal through your backend (CORS); only the public session endpoint works from the browser.
- Send Idempotency-Key when creating sessions.
- Session total must be at least 1.00 in the session currency (CHECKOUT_AMOUNT_TOO_LOW); Tron needs $300.
- Webhook data carries your session metadata — put your order id there.
- Docs: https://bitpal.io/docs/ · MCP: https://mcp.bitpal.io/mcp

Chains & contracts체인 & 컨트랙트

Chains & identifiers체인 & 식별자

The pay_chain / allowed_pay_chains values are CAIP-2 identifiers. Use the value from the column matching your API key's mode.

pay_chain / allowed_pay_chains 값은 CAIP-2 식별자입니다. API 키 모드에 맞는 열의 값을 사용하세요.

Network네트워크 Tokens토큰 Decimals소수 pay_chain — mainnet메인넷 pay_chain — testnet테스트넷
BaseUSDC6eip155:8453eip155:84532
ArbitrumUSDC6eip155:42161eip155:421614
BNB Smart ChainUSDT18eip155:56eip155:97
TronUSDT6tron:0x2b6653dctron:0xcd8690dc
SolanaUSDC · USDT6solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdpsolana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1
PolygonJPYC18eip155:137eip155:80002
KaiaJPYC18eip155:8217eip155:1001
JPYC (Polygon/Kaia) settles 1:1 to JPY — no FX conversion.JPYC(Polygon/Kaia)는 JPY와 1:1로 정산되며, 환율 변환은 없습니다.
Test keys (bp_test_…) accept only testnet values; live keys only mainnet.테스트 키(bp_test_…)는 테스트넷 값만, 라이브 키는 메인넷 값만 받습니다.

Contract addresses컨트랙트 주소

The deposit-address contracts (EVM/Tron deterministic forwarder factory, Solana program). Published here as each network goes live.

입금 주소용 컨트랙트입니다(EVM/Tron은 결정적 forwarder factory, Solana는 프로그램). 각 네트워크가 라이브되는 대로 여기에 게시합니다.

Network네트워크 Contract / program컨트랙트 / 프로그램 Mainnet메인넷 Testnet테스트넷
BaseForwarder factoryForwarder factory0xC5d358f4346d59D8eCDb5cE7DE77367b068a7E7c0x5bA4F84B3002B9F94670f296134aE4f823662B9f
ArbitrumForwarder factory0xC7989977010890AE78938a84E487573a1D9933660x38B54b7F8a032E8087cd04f12a7Bdb9011c5Bd95
BNBForwarder factory0xC7989977010890AE78938a84E487573a1D9933660xeD24d9ED87a9937528520413891e8f78EF7ED3C5
TronForwarder factoryTH2t7eXXVbmHxoBvWk5YTAGVtMTomcv6ieTBRQHfn5cRiEd2htYF7Xwae61RZD32T4ee
SolanaProgram ID2cZyQxMgWToiCWho9q24VyAvs6xy7t3JSEEwNpGjf4ec2cZyQxMgWToiCWho9q24VyAvs6xy7t3JSEEwNpGjf4ec
PolygonForwarder factory0xA2F1bB858DBc25355D8cb5180D7e58bDC2D696De0x42A70E1a47C9AAeF10923f5345b2ad64D2F05D3F
KaiaForwarder factory0xA2F1bB858DBc25355D8cb5180D7e58bDC2D696De0xf07BB5243636D8C834b31f224E2567E6e6aF38ce

These are the factories that derive deposit addresses. Always treat the deposit address returned by BitPal as authoritative for a given order — funds are cryptographically committed to your destination.

위 컨트랙트는 입금 주소를 파생하는 factory입니다. 개별 주문에서는 항상 BitPal이 반환한 입금 주소를 기준으로 삼으세요 — 자금은 암호학적으로 판매자의 정산 목적지에 고정되어 있습니다.

Refunds & edge cases환불 & 예외

Payments are push transfers, so a buyer can pay the wrong amount or token. BitPal resolves each case automatically to the buyer's refund address (set at pay time, committed & immutable).

결제는 구매자가 직접 보내는 push 송금이라 금액이나 토큰이 틀릴 수 있습니다. BitPal은 각 경우를 구매자의 환불 주소(결제 시 지정되며 고정·변경 불가)로 자동 처리합니다.

Case경우What happens처리
Overpaid과다 입금Settles the exact amount; surplus refunded.정확한 금액이 정산되고, 초과분은 환불됩니다.
Underpaid과소 입금Open for a top-up; refunded if the deadline passes.추가 입금을 기다리며, 기한이 지나면 환불됩니다.
Wrong token잘못된 토큰Returned to the buyer; order stays open.구매자에게 반환되며, 주문은 계속 결제할 수 있습니다.
Expired만료No funds arrived — address reclaimed.입금이 없었습니다 — 주소는 회수됩니다.
Late, full amount기한 뒤 전액Accepted as a late payment for up to 7 days (late: true); settles normally. Not for sessions that already had a wrong-token return or underpayment refund; after the cutoff it is refunded (late_deposit).최대 7일까지 늦은 결제로 인정되며(late: true) 정상적으로 정산됩니다. 이미 잘못된 토큰 반환이나 과소 입금 환불이 있었던 세션은 제외되며, 인정 마감 이후 도착분은 환불됩니다(late_deposit).
Late, full amount, late_payments: "refund"기한 뒤 전액, late_payments: "refund"Not accepted. Refunded automatically about 7 days after the deadline (late_deposit) — the non-custodial deposit contract does not allow an earlier refund.결제로 인정되지 않습니다. 기한 약 7일 뒤 자동 환불됩니다(late_deposit) — 비수탁 입금 컨트랙트라 더 일찍 돌려줄 수 없습니다.
Late, partial기한 뒤 일부Refunded (late_deposit).환불됩니다(late_deposit).
Customer return (completed payment)완료된 결제 환불(고객 반품 등)No refund API — BitPal never holds the funds. Refund in Console → Sessions → session → Refund: send from your own wallet, record the transaction, and BitPal verifies it and sends checkout.refund.recorded (merchant). You pay the gas and the refund; the BitPal fee is not returned.환불 API는 없습니다 — BitPal은 자금을 보관하지 않습니다. 콘솔 → 세션 → 해당 세션 → 환불에서 판매자 지갑으로 직접 보내고 트랜잭션을 기록하면, BitPal이 온체인에서 확인한 뒤 checkout.refund.recorded(merchant)를 보냅니다. 가스비와 환불액은 판매자가 부담하며, BitPal 수수료는 돌려주지 않습니다.

Test mode & faucet테스트 모드 & faucet

  • Keys are environment-scoped: bp_test_… on testnets, bp_live_… on mainnet.
  • In test mode the hosted page shows a faucet — the buyer mints mock tokens to run a real end-to-end payment. (Gas is separate; the faucet sends tokens only.)
  • 키는 환경별로 구분됩니다: bp_test_…는 테스트넷, bp_live_…는 메인넷에서 동작합니다.
  • 테스트 모드의 호스티드 결제 페이지에는 faucet이 표시됩니다 — 구매자가 mock 토큰을 받아 실제 end-to-end 결제를 해볼 수 있습니다. (가스는 별도이며, faucet은 토큰만 보내 줍니다.)
BitPal — non-custodial crypto checkout. Amounts are quoted in the line item currency; settlement is same-chain, same-token. BitPal — 비수탁 크립토 결제. 금액은 line item 통화 기준이며, 정산은 같은 체인·같은 토큰으로 이루어집니다.

엔드포인트별 설명은 API 코드의 OpenAPI 스펙에서 생성되어 영어로 제공됩니다. 필드·파라미터 이름과 예시는 그대로 사용하면 됩니다.

API referenceAPI 레퍼런스

Every public endpoint, generated from the OpenAPI 3.1.0 spec (version 1.0.0). Download it: openapi.json.

모든 공개 엔드포인트를 OpenAPI 3.1.0 스펙(버전 1.0.0)에서 생성했습니다. 스펙 다운로드: openapi.json.

Basics기본 사항

Topic항목Details내용
Base URLhttps://api.bitpal.io — test and live share the host; the API key prefix selects the mode.https://api.bitpal.io — 테스트와 라이브가 같은 호스트를 쓰며, API 키 접두사로 모드가 정해집니다.
Auth인증Authorization: Bearer bp_test_… (test) or bp_live_… (live). Keep keys on your server.Authorization: Bearer bp_test_…(테스트) 또는 bp_live_…(라이브). 키는 서버에만 보관하세요.
Format형식JSON. Success: { "data": … } (authenticated endpoints add meta: { mode, requestId }). Amounts are strings.JSON. 성공 시 { "data": … }(인증이 필요한 엔드포인트는 meta: { mode, requestId } 추가). 금액은 문자열입니다.
Errors에러{ "error", "code", "retryable", "trace_id", "details"? } — error is the human-readable message; branch on code. See Errors.{ "error", "code", "retryable", "trace_id", "details"? } — error는 사람이 읽는 메시지이니 code로 분기하세요. 에러 참고.
Idempotency멱등성Idempotency-Key (8–128 chars) is honoured on every authenticated merchant POST; endpoints that do not enforce it say so.Idempotency-Key(8–128자)는 인증된 모든 가맹점 POST에서 지원되며, 적용되지 않는 엔드포인트는 따로 표시되어 있습니다.

Checkout sessions

Create a session per order, send the buyer to the hosted page (or build your own UI), and track it.

POST /v1/checkout/sessions — Create a checkout session

Creates a session for one order and returns the hosted checkout_url. Line items take either amount (plain decimal string, e.g. "29") or amount_atomic (6-decimal integer string), or reference a price_id. **Minimum amount:** the session total (sum of amount × quantity) must be at least 1.00 in the session currency (USDC/USDT: $1, JPYC: ¥1); a lower total returns 400 CHECKOUT_AMOUNT_TOO_LOW. Some networks also have a higher per-network minimum (Tron: currently $300, testnet included; JPYC: ¥100) — a network whose minimum is above the total is not offered to the buyer, and requesting a deposit address on it returns 400 CHECKOUT_BELOW_MIN_PAYMENT. **Payout wallet:** you need a payout wallet registered in the session's mode (test/live). With pay_chain, a wallet for that network's type (EVM, Tron or Solana) is required. With allowed_pay_chains / allowed_assets, at least one listed network must have a wallet of its type, and the buyer is only offered networks you can receive on. Without any network restriction, any payout wallet is enough: with an EVM wallet the buyer is offered Base USDC as before; without one, the networks of the wallet types you registered (if the total is below the minimum of all of them, 400 CHECKOUT_BELOW_MIN_PAYMENT). No usable payout wallet returns 400 SELLER_WALLET_NOT_CONFIGURED.

Auth:인증: API key — Authorization: Bearer bp_test_… / bp_live_… (server only)API 키 — Authorization: Bearer bp_test_… / bp_live_… (서버 전용)

Parameters파라미터

Parameter파라미터In위치Type타입Required필수Description설명
Idempotency-KeyheaderstringNo아니요8–128 characters, unique per operation. A retry with the same key and body within 24 hours returns the original response with X-Idempotency-Replayed: true. (8–128 chars)

Request body요청 본문 (CreateCheckoutSessionRequest)

Field필드Type타입Required필수Description설명
line_itemsarray of (object | any | any)Yes예Raw items (name, currency, amount XOR amount_atomic) or price items (price_id). Do not mix the two kinds. The total (sum of amount × quantity) must be at least 1.00 in the session currency, otherwise 400 CHECKOUT_AMOUNT_TOO_LOW. (≥ 1 items)
currencystringNo아니요Session currency. Defaults to the line items’ currency. (≥ 1 chars)
success_urlstring (uri)No아니요Where the buyer returns after paying. {CHECKOUT_SESSION_ID} is replaced with the session id. (≤ 2048 chars)
cancel_urlstring (uri)No아니요Where the buyer goes when cancelling. (≤ 2048 chars)
customer_emailstring (email)No아니요Buyer email. Pre-fills the receipt form on the payment completion screen; no receipt is sent automatically — the buyer requests it there.
metadatamapNo아니요Your key/value data, echoed back on the session and in webhooks (data.metadata). Reserved keys BitPal writes itself (_bitpal, payer_email, allowed_pay_chains, allowed_assets) are not returned — do not use them.
expires_in_minutesintegerNo아니요Payment window in minutes (1–60). Default 15. (1–60, default 15)
tolerance_percentstringNo아니요Deprecated. Underpayments are never accepted — a short payment becomes unresolved_underpaid and can be topped up to the same address. Only "0" is accepted; any other value returns 400.
pay_chainstringNo아니요Fix the payment network: CAIP-2 (eip155:84532) or a short name (base), which resolves to the testnet in test mode and the mainnet in live mode. Mutually exclusive with allowed_pay_chains.
allowed_pay_chainsarray of stringNo아니요Networks the buyer may choose from (CAIP-2 or short names, resolved like pay_chain). (1–8 items)
allowed_assetsarray of objectNo아니요Allowed (chain, token) cells. Takes precedence over allowed_pay_chains. (1–16 items)
late_payments"accept" | "refund"No아니요What to do with a full payment that arrives after the payment deadline. accept (default): accepted as a late payment for up to 7 days (checkout.session.paid with late: true). refund: never accepted — it is refunded automatically to the buyer about 7 days after the deadline (refund kind late_deposit); the non-custodial deposit contract does not allow an earlier refund.
{
  "line_items": [
    {
      "name": "Pro plan",
      "amount": "29",
      "currency": "USDC"
    }
  ],
  "pay_chain": "eip155:84532",
  "expires_in_minutes": 30,
  "success_url": "https://your.store/thanks?session={CHECKOUT_SESSION_ID}"
}

Response응답 201 — Session created.

{
  "data": {
    "id": "cs_Q2FxKhomCEju_OqvKfrQsTOh",
    "checkout_url": "https://bitpal.io/pay/cs_Q2FxKhomCEju_OqvKfrQsTOh?token=<session_token>",
    "session_token": "<session_token>",
    "status": "created",
    "amount_total": "29000000",
    "currency": "USDC",
    "expires_at": "2026-11-06T07:58:28.776Z",
    "tax": {
      "tax_amount": "0",
      "total_with_tax": "29000000"
    }
  },
  "meta": {
    "mode": "TEST",
    "requestId": "ec091faf-…"
  }
}

Errors:에러: 400 · 401 · 403 · 409 · 422 · 429 — see Errors.에러 참고.

GET /v1/checkout/sessions — List checkout sessions

Newest first, in the API key’s mode (test/live).

Auth:인증: API key — Authorization: Bearer bp_test_… / bp_live_… (server only)API 키 — Authorization: Bearer bp_test_… / bp_live_… (서버 전용)

Parameters파라미터

Parameter파라미터In위치Type타입Required필수Description설명
statusquerystringNo아니요Filter by status, or succeeded for all successful states.
limitqueryintegerNo아니요1–100, default 50
offsetqueryintegerNo아니요≥ 0, default 0

Response응답 200 — Sessions.

{
  "data": [
    {
      "id": "string",
      "status": "string",
      "mode": "TEST",
      "amount_total": "1000000",
      "amount_received": "string",
      "currency": "string",
      "pay_chain": "string",
      "pay_asset": "string",
      "token_decimals": 0,
      "payment_method": "string",
      "payer_wallet": "string",
      "seller_wallet": "string",
      "tolerance_percent": "string",
      "late_payments": "accept",
      "deposit_address": "string",
      "refund_address": "string",
      "received_token": "string",
      "received_token_symbol": "string",
      "fee_amount": "string",
      "fee_rate": "string",
      "net_amount": "string",
      "fee_amount_token": "string",
      "net_amount_token": "string",
      "tx_hash": "string",
      "deposit_tx_hash": "string",
      "sweep_tx_hash": "string",
      "success_url": "string",
      "cancel_url": "string",
      "metadata": {},
      "payment_source": "string",
      "payment_link_id": "string",
      "expires_at": "2026-11-06T07:00:00.000Z",
      "paid_at": "2026-11-06T07:00:00.000Z",
      "settled_at": "2026-11-06T07:00:00.000Z",
      "created_at": "2026-11-06T07:00:00.000Z",
      "raw_status": "string"
    }
  ],
  "meta": {
    "mode": "TEST",
    "requestId": "string",
    "total": 0,
    "limit": 0,
    "offset": 0
  }
}

Errors:에러: 401 — see Errors.에러 참고.

GET /v1/checkout/sessions/{id} — Retrieve a checkout session

Server-side source of truth for an order, together with webhooks.

Auth:인증: API key — Authorization: Bearer bp_test_… / bp_live_… (server only)API 키 — Authorization: Bearer bp_test_… / bp_live_… (서버 전용)

Parameters파라미터

Parameter파라미터In위치Type타입Required필수Description설명
idpathstringYes예Checkout session id (cs_…).

Response응답 200 — The session.

{
  "data": {
    "id": "string",
    "status": "string",
    "mode": "TEST",
    "amount_total": "1000000",
    "amount_received": "string",
    "currency": "string",
    "pay_chain": "string",
    "pay_asset": "string",
    "token_decimals": 0,
    "payment_method": "string",
    "payer_wallet": "string",
    "seller_wallet": "string",
    "tolerance_percent": "string",
    "late_payments": "accept",
    "deposit_address": "string",
    "refund_address": "string",
    "received_token": "string",
    "received_token_symbol": "string",
    "fee_amount": "string",
    "fee_rate": "string",
    "net_amount": "string",
    "fee_amount_token": "string",
    "net_amount_token": "string",
    "tx_hash": "string",
    "deposit_tx_hash": "string",
    "sweep_tx_hash": "string",
    "success_url": "string",
    "cancel_url": "string",
    "metadata": {},
    "payment_source": "string",
    "payment_link_id": "string",
    "expires_at": "2026-11-06T07:00:00.000Z",
    "paid_at": "2026-11-06T07:00:00.000Z",
    "settled_at": "2026-11-06T07:00:00.000Z",
    "created_at": "2026-11-06T07:00:00.000Z",
    "line_items": [
      {
        "id": "string",
        "name": "string",
        "amount": "1000000",
        "currency": "string",
        "quantity": 0,
        "price_id": "string",
        "product_id": "string",
        "created_at": "2026-11-06T07:00:00.000Z"
      }
    ]
  },
  "meta": {
    "mode": "TEST",
    "requestId": "string",
    "total": 0,
    "limit": 0,
    "offset": 0
  }
}

Errors:에러: 401 · 404 — see Errors.에러 참고.

GET /v1/checkout/sessions/{id}/status — Get session status

Lightweight status for polling from your server. Requires your API key (N13 — it is not a public endpoint). Buyer-facing pages should use GET /v1/checkout/sessions/{id}/public.

Auth:인증: API key — Authorization: Bearer bp_test_… / bp_live_… (server only)API 키 — Authorization: Bearer bp_test_… / bp_live_… (서버 전용)

Parameters파라미터

Parameter파라미터In위치Type타입Required필수Description설명
idpathstringYes예Checkout session id (cs_…).

Response응답 200 — Status.

{
  "data": {
    "status": "awaiting_deposit",
    "paid_at": null
  }
}

Errors:에러: 404 — see Errors.에러 참고.

GET /v1/checkout/sessions/{id}/public — Get the public session view

Buyer-facing view used by the checkout page: payment options, deposit address once issued, refunds. No authentication; CORS-open.

Auth:인증: None — public endpoint없음 — 공개 엔드포인트

Parameters파라미터

Parameter파라미터In위치Type타입Required필수Description설명
idpathstringYes예Checkout session id (cs_…).

Response응답 200 — Public view.

{
  "data": {
    "id": "string",
    "status": "string",
    "token_decimals": 0,
    "pay_amount": "string",
    "mode": "TEST",
    "chain_id": 0,
    "token_address": "string",
    "payment_options": [
      {
        "caip2": "string",
        "chain_id": 0,
        "token_symbol": "string",
        "token_address": "string",
        "decimals": 0,
        "amount_atomic": "string",
        "available": true,
        "unavailable_reason": "coming_soon"
      }
    ],
    "currency": "string",
    "amount_total": "1000000",
    "amount_received": "string",
    "chain": "string",
    "tx_hash": "string",
    "release_tx_hash": "string",
    "expires_at": "2026-11-06T07:00:00.000Z",
    "success_url": "string",
    "cancel_url": "string",
    "fee_amount": "string",
    "fee_rate": "string",
    "net_amount": "string",
    "tax_amount": null,
    "tax_rate": null,
    "pay_chain": "string",
    "pay_asset": "string",
    "deposit_address": "string",
    "deposit_owner": "string",
    "payment_deadline_at": "2026-11-06T07:00:00.000Z",
    "payment_method": "string",
    "payer_wallet": "string",
    "seller_wallet": "string",
    "tolerance_percent": "string",
    "paid_at": "2026-11-06T07:00:00.000Z",
    "settled_at": "2026-11-06T07:00:00.000Z",
    "created_at": "2026-11-06T07:00:00.000Z",
    "line_items": [
      {
        "name": "string",
        "amount": "1000000",
        "currency": "string",
        "quantity": 0
      }
    ],
    "payment_source": "string",
    "refunds": [
      {
        "kind": "string",
        "amount": "string",
        "token_symbol": "string",
        "decimals": 0,
        "tx_hash": "string",
        "caip2": "string"
      }
    ]
  },
  "meta": {
    "mode": "TEST",
    "requestId": "string",
    "total": 0,
    "limit": 0,
    "offset": 0
  }
}

Errors:에러: 404 — see Errors.에러 참고.

POST /v1/checkout/sessions/{id}/deposit-address — Issue the deposit address

Fixes the buyer’s network, token and refund address, and returns the one-time deposit address with the exact amount. Calling again returns the same address and keeps the first refund address. Requires the session token (header X-Session-Token or body session_token).

Auth:인증: Session token — X-Session-Token header or session_token body field (no API key)세션 토큰 — X-Session-Token 헤더 또는 session_token 본문 필드 (API 키 없음)

Parameters파라미터

Parameter파라미터In위치Type타입Required필수Description설명
idpathstringYes예Checkout session id (cs_…).
Idempotency-KeyheaderstringNo아니요Accepted but not yet enforced on this endpoint — retries are not de-duplicated by key. (8–128 chars)
X-Session-TokenheaderstringNo아니요Session token from the create-session response. Alternative to the session_token body field.

Request body요청 본문 (IssueDepositAddressRequest)

Field필드Type타입Required필수Description설명
session_tokenstringNo아니요From the create-session response. May be sent as the X-Session-Token header instead.
refundAddressstringYes예Buyer refund address in the chosen network’s format (EVM 0x…, Tron T…, Solana base58). Cannot change later.
chainstringNo아니요CAIP-2 or short name of the chosen network. Required for multi-option sessions. (≥ 1 chars)
tokenstringNo아니요Token symbol of the chosen asset, e.g. USDC. Required for multi-option sessions. (≥ 1 chars)
{
  "session_token": "<session_token>",
  "refundAddress": "0xa0Ee7A142d267C1f36714E4a8F75612F20a79720",
  "chain": "eip155:84532",
  "token": "USDC"
}

Response응답 200 — Deposit address.

{
  "data": {
    "sessionId": "cs_…",
    "chain": "eip155:84532",
    "depositAddress": "0x6b19e871c934aB52775B6712e110f3e4b0626347",
    "expectedToken": "0x943b94dacf1507f2658f1cfe103ac36ebc13ef65",
    "expectedAmount": "29000000",
    "feeAmount": "435000",
    "refundAddress": "0xa0Ee7A142d267C1f36714E4a8F75612F20a79720",
    "deadline": "1793951938",
    "expiresAt": "2026-11-06T07:58:28.776Z",
    "depositOwner": null
  }
}

Errors:에러: 400 · 401 · 404 · 409 · 410 — see Errors.에러 참고.

GET /v1/checkout/sessions/{id}/refunds — List refunds for a session

Refunds recorded for this session: automatic on-chain refunds from the deposit address (overpayment, expired underpayment, wrong token, late deposit) and refunds you sent from your own wallet and recorded in the console (kind: merchant). There is no API to make a refund — BitPal is non-custodial; refund a completed payment from Console → Sessions → session → Refund.

Auth:인증: API key — Authorization: Bearer bp_test_… / bp_live_… (server only)API 키 — Authorization: Bearer bp_test_… / bp_live_… (서버 전용)

Parameters파라미터

Parameter파라미터In위치Type타입Required필수Description설명
idpathstringYes예Checkout session id (cs_…).

Response응답 200 — Refunds.

{
  "data": [
    {
      "id": "string",
      "kind": "string",
      "token": "string",
      "token_symbol": "string",
      "amount": "string",
      "amount_display": "string",
      "decimals": 0,
      "refund_address": "string",
      "tx_hash": "string",
      "caip2": "string",
      "block_number": "string",
      "created_at": "2026-11-06T07:00:00.000Z",
      "from_address": "string",
      "reason_code": "string",
      "note": "string",
      "explorer_url": "string"
    }
  ],
  "meta": {
    "mode": "TEST",
    "requestId": "string",
    "total": 0,
    "limit": 0,
    "offset": 0
  }
}

Errors:에러: 401 · 404 — see Errors.에러 참고.

GET /v1/checkout/fee-preview — Preview the fee for an amount

Pass exactly one of amount or amount_atomic. The response is always atomic.

Auth:인증: API key — Authorization: Bearer bp_test_… / bp_live_… (server only)API 키 — Authorization: Bearer bp_test_… / bp_live_… (서버 전용)

Parameters파라미터

Parameter파라미터In위치Type타입Required필수Description설명
amountquerystringNo아니요Decimal string, e.g. "100".
amount_atomicquerystringNo아니요6-decimal integer string, e.g. "100000000".

Response응답 200 — Fee breakdown.

{
  "data": {
    "amount": "100000000",
    "fee_rate": "0.015",
    "fee_amount": "1500000",
    "net_amount": "98500000"
  }
}

Errors:에러: 400 · 401 — see Errors.에러 참고.

Products & prices

Catalog objects. Prices are immutable.

POST /v1/checkout/products — Create a product

Auth:인증: API key — Authorization: Bearer bp_test_… / bp_live_… (server only)API 키 — Authorization: Bearer bp_test_… / bp_live_… (서버 전용)

Parameters파라미터

Parameter파라미터In위치Type타입Required필수Description설명
Idempotency-KeyheaderstringNo아니요8–128 characters, unique per operation. A retry with the same key and body within 24 hours returns the original response with X-Idempotency-Replayed: true. (8–128 chars)

Request body요청 본문 (CreateProductRequest)

Field필드Type타입Required필수Description설명
namestringYes예1–200 chars
descriptionstringNo아니요≤ 2000 chars
image_urlstring (uri)No아니요≤ 2048 chars
metadatamapNo아니요
{
  "name": "Pro plan"
}

Response응답 201 — Product created.

{
  "data": {
    "id": "string",
    "name": "string",
    "description": "string",
    "image_url": "string",
    "status": "active",
    "metadata": {},
    "mode": "TEST",
    "created_at": "2026-11-06T07:00:00.000Z",
    "updated_at": "2026-11-06T07:00:00.000Z"
  },
  "meta": {
    "mode": "TEST",
    "requestId": "string",
    "total": 0,
    "limit": 0,
    "offset": 0
  }
}

Errors:에러: 400 · 401 · 403 — see Errors.에러 참고.

GET /v1/checkout/products — List products

Auth:인증: API key — Authorization: Bearer bp_test_… / bp_live_… (server only)API 키 — Authorization: Bearer bp_test_… / bp_live_… (서버 전용)

Parameters파라미터

Parameter파라미터In위치Type타입Required필수Description설명
statusquery"active" | "inactive"No아니요
limitqueryintegerNo아니요1–100, default 50
offsetqueryintegerNo아니요≥ 0, default 0

Response응답 200 — Products.

{
  "data": [
    {
      "id": "string",
      "name": "string",
      "description": "string",
      "image_url": "string",
      "status": "active",
      "metadata": {},
      "mode": "TEST",
      "created_at": "2026-11-06T07:00:00.000Z",
      "updated_at": "2026-11-06T07:00:00.000Z"
    }
  ],
  "meta": {
    "mode": "TEST",
    "requestId": "string",
    "total": 0,
    "limit": 0,
    "offset": 0
  }
}

Errors:에러: 400 · 401 — see Errors.에러 참고.

GET /v1/checkout/products/{id} — Retrieve a product

Auth:인증: API key — Authorization: Bearer bp_test_… / bp_live_… (server only)API 키 — Authorization: Bearer bp_test_… / bp_live_… (서버 전용)

Parameters파라미터

Parameter파라미터In위치Type타입Required필수Description설명
idpathstringYes예Product id (prod_…).

Response응답 200 — The product.

{
  "data": {
    "id": "string",
    "name": "string",
    "description": "string",
    "image_url": "string",
    "status": "active",
    "metadata": {},
    "mode": "TEST",
    "created_at": "2026-11-06T07:00:00.000Z",
    "updated_at": "2026-11-06T07:00:00.000Z"
  },
  "meta": {
    "mode": "TEST",
    "requestId": "string",
    "total": 0,
    "limit": 0,
    "offset": 0
  }
}

Errors:에러: 400 · 401 · 404 — see Errors.에러 참고.

PATCH /v1/checkout/products/{id} — Update a product

Auth:인증: API key — Authorization: Bearer bp_test_… / bp_live_… (server only)API 키 — Authorization: Bearer bp_test_… / bp_live_… (서버 전용)

Parameters파라미터

Parameter파라미터In위치Type타입Required필수Description설명
idpathstringYes예Product id (prod_…).

Request body요청 본문 (UpdateProductRequest)

Field필드Type타입Required필수Description설명
namestringNo아니요1–200 chars
descriptionstring | nullNo아니요≤ 2000 chars
image_urlstring (uri) | nullNo아니요≤ 2048 chars
metadatamapNo아니요
status"active" | "inactive"No아니요

Response응답 200 — The updated product.

{
  "data": {
    "id": "string",
    "name": "string",
    "description": "string",
    "image_url": "string",
    "status": "active",
    "metadata": {},
    "mode": "TEST",
    "created_at": "2026-11-06T07:00:00.000Z",
    "updated_at": "2026-11-06T07:00:00.000Z"
  },
  "meta": {
    "mode": "TEST",
    "requestId": "string",
    "total": 0,
    "limit": 0,
    "offset": 0
  }
}

Errors:에러: 400 · 401 · 403 · 404 — see Errors.에러 참고.

POST /v1/checkout/products/{id}/prices — Create a price

Prices are immutable: to change an amount, create a new price. Pass exactly one of amount (decimal string) or amount_atomic (6-decimal integer string). The currency is derived from token_symbol.

Auth:인증: API key — Authorization: Bearer bp_test_… / bp_live_… (server only)API 키 — Authorization: Bearer bp_test_… / bp_live_… (서버 전용)

Parameters파라미터

Parameter파라미터In위치Type타입Required필수Description설명
idpathstringYes예Product id (prod_…).
Idempotency-KeyheaderstringNo아니요8–128 characters, unique per operation. A retry with the same key and body within 24 hours returns the original response with X-Idempotency-Replayed: true. (8–128 chars)

Request body요청 본문 (CreatePriceRequest)

Field필드Type타입Required필수Description설명
amountstringNo아니요Decimal string, e.g. "29". Mutually exclusive with amount_atomic.
amount_atomicstringNo아니요6-decimal integer string ("29000000" = 29).
token_symbol"USDC" | "USDT" | "JPYC"No아니요The price’s asset. The currency is derived from it. (default "USDC")
billing_type"one_time"No아니요Only one_time for now. (default "one_time")
nicknamestringNo아니요Display name. (≤ 200 chars)
lookup_keystringNo아니요Your own stable key for this price. (1–200 chars)
{
  "amount": "29",
  "token_symbol": "USDC"
}

Response응답 201 — Price created.

{
  "data": {
    "id": "string",
    "product_id": "string",
    "amount_atomic": "1000000",
    "currency": "string",
    "token_symbol": "string",
    "billing_type": "one_time",
    "nickname": "string",
    "lookup_key": "string",
    "mode": "TEST",
    "created_at": "2026-11-06T07:00:00.000Z"
  },
  "meta": {
    "mode": "TEST",
    "requestId": "string",
    "total": 0,
    "limit": 0,
    "offset": 0
  }
}

Errors:에러: 400 · 401 · 403 · 404 — see Errors.에러 참고.

GET /v1/checkout/products/{id}/prices — List a product’s prices

Auth:인증: API key — Authorization: Bearer bp_test_… / bp_live_… (server only)API 키 — Authorization: Bearer bp_test_… / bp_live_… (서버 전용)

Parameters파라미터

Parameter파라미터In위치Type타입Required필수Description설명
idpathstringYes예Product id (prod_…).
limitqueryintegerNo아니요1–100, default 50
offsetqueryintegerNo아니요≥ 0, default 0

Response응답 200 — Prices.

{
  "data": [
    {
      "id": "string",
      "product_id": "string",
      "amount_atomic": "1000000",
      "currency": "string",
      "token_symbol": "string",
      "billing_type": "one_time",
      "nickname": "string",
      "lookup_key": "string",
      "mode": "TEST",
      "created_at": "2026-11-06T07:00:00.000Z"
    }
  ],
  "meta": {
    "mode": "TEST",
    "requestId": "string",
    "total": 0,
    "limit": 0,
    "offset": 0
  }
}

Errors:에러: 400 · 401 · 404 — see Errors.에러 참고.

GET /v1/checkout/prices/{id} — Retrieve a price

Auth:인증: API key — Authorization: Bearer bp_test_… / bp_live_… (server only)API 키 — Authorization: Bearer bp_test_… / bp_live_… (서버 전용)

Parameters파라미터

Parameter파라미터In위치Type타입Required필수Description설명
idpathstringYes예Price id (price_…).

Response응답 200 — The price.

{
  "data": {
    "id": "string",
    "product_id": "string",
    "amount_atomic": "1000000",
    "currency": "string",
    "token_symbol": "string",
    "billing_type": "one_time",
    "nickname": "string",
    "lookup_key": "string",
    "mode": "TEST",
    "created_at": "2026-11-06T07:00:00.000Z"
  },
  "meta": {
    "mode": "TEST",
    "requestId": "string",
    "total": 0,
    "limit": 0,
    "offset": 0
  }
}

Errors:에러: 401 · 404 — see Errors.에러 참고.

PATCH /v1/checkout/prices/{id} — Update a price’s nickname

Only nickname can change. Sending an amount/currency field returns PRICE_IMMUTABLE_FIELD.

Auth:인증: API key — Authorization: Bearer bp_test_… / bp_live_… (server only)API 키 — Authorization: Bearer bp_test_… / bp_live_… (서버 전용)

Parameters파라미터

Parameter파라미터In위치Type타입Required필수Description설명
idpathstringYes예Price id (price_…).

Request body요청 본문 (UpdatePriceRequest)

Field필드Type타입Required필수Description설명
nicknamestring | nullNo아니요≤ 200 chars

Response응답 200 — The updated price.

{
  "data": {
    "id": "string",
    "product_id": "string",
    "amount_atomic": "1000000",
    "currency": "string",
    "token_symbol": "string",
    "billing_type": "one_time",
    "nickname": "string",
    "lookup_key": "string",
    "mode": "TEST",
    "created_at": "2026-11-06T07:00:00.000Z"
  },
  "meta": {
    "mode": "TEST",
    "requestId": "string",
    "total": 0,
    "limit": 0,
    "offset": 0
  }
}

Errors:에러: 400 · 401 · 403 · 404 — see Errors.에러 참고.

Payment links

Shareable hosted URLs for a price.

POST /v1/checkout/payment-links — Create a payment link

A shareable hosted URL for one price. The link ↔ price binding is fixed after creation.

Auth:인증: API key — Authorization: Bearer bp_test_… / bp_live_… (server only)API 키 — Authorization: Bearer bp_test_… / bp_live_… (서버 전용)

Parameters파라미터

Parameter파라미터In위치Type타입Required필수Description설명
Idempotency-KeyheaderstringNo아니요8–128 characters, unique per operation. A retry with the same key and body within 24 hours returns the original response with X-Idempotency-Replayed: true. (8–128 chars)

Request body요청 본문 (CreatePaymentLinkRequest)

Field필드Type타입Required필수Description설명
price_idstringYes예The price this link sells (price_…).
titlestringNo아니요1–200 chars
descriptionstringNo아니요≤ 2000 chars
collect_emailbooleanNo아니요Ask the buyer for an email. Default true. (default true)
email_requiredbooleanNo아니요Require the email: the hosted link page asks for it and no checkout is created without it. Default true; needs collect_email. (default true)
allowed_pay_chainsarray of stringNo아니요Networks the buyer may choose from (CAIP-2 or short names). (1–8 items)
allowed_assetsarray of objectNo아니요Allowed (chain, token) cells. Takes precedence over allowed_pay_chains. (1–16 items)
success_urlstring (uri)No아니요≤ 2048 chars
cancel_urlstring (uri)No아니요≤ 2048 chars
max_usesintegerNo아니요Stop accepting payments after this many paid uses. (≥ 1)
expires_atstring (date-time)No아니요ISO-8601 time after which the link stops working.
metadatamapNo아니요
{
  "price_id": "price_…",
  "allowed_pay_chains": [
    "eip155:8453"
  ]
}

Response응답 201 — Link created.

{
  "data": {
    "id": "string",
    "share_token": "string",
    "price_id": "string",
    "title": "string",
    "description": "string",
    "collect_email": true,
    "email_required": true,
    "allowed_pay_chains": [
      "string"
    ],
    "allowed_assets": [
      {
        "caip2": "string",
        "token": "string"
      }
    ],
    "success_url": "string",
    "cancel_url": "string",
    "status": "active",
    "max_uses": 0,
    "paid_use_count": 0,
    "expires_at": "2026-11-06T07:00:00.000Z",
    "metadata": {},
    "mode": "TEST",
    "created_at": "2026-11-06T07:00:00.000Z",
    "updated_at": "2026-11-06T07:00:00.000Z"
  },
  "meta": {
    "mode": "TEST",
    "requestId": "string",
    "total": 0,
    "limit": 0,
    "offset": 0
  }
}

Errors:에러: 400 · 401 · 403 · 404 — see Errors.에러 참고.

GET /v1/checkout/payment-links — List payment links

Auth:인증: API key — Authorization: Bearer bp_test_… / bp_live_… (server only)API 키 — Authorization: Bearer bp_test_… / bp_live_… (서버 전용)

Parameters파라미터

Parameter파라미터In위치Type타입Required필수Description설명
statusquery"active" | "inactive"No아니요
limitqueryintegerNo아니요1–100, default 50
offsetqueryintegerNo아니요≥ 0, default 0

Response응답 200 — Links.

{
  "data": [
    {
      "id": "string",
      "share_token": "string",
      "price_id": "string",
      "title": "string",
      "description": "string",
      "collect_email": true,
      "email_required": true,
      "allowed_pay_chains": [
        "string"
      ],
      "allowed_assets": [
        {
          "caip2": "string",
          "token": "string"
        }
      ],
      "success_url": "string",
      "cancel_url": "string",
      "status": "active",
      "max_uses": 0,
      "paid_use_count": 0,
      "expires_at": "2026-11-06T07:00:00.000Z",
      "metadata": {},
      "mode": "TEST",
      "created_at": "2026-11-06T07:00:00.000Z",
      "updated_at": "2026-11-06T07:00:00.000Z"
    }
  ],
  "meta": {
    "mode": "TEST",
    "requestId": "string",
    "total": 0,
    "limit": 0,
    "offset": 0
  }
}

Errors:에러: 400 · 401 — see Errors.에러 참고.

GET /v1/checkout/payment-links/{id} — Retrieve a payment link

Auth:인증: API key — Authorization: Bearer bp_test_… / bp_live_… (server only)API 키 — Authorization: Bearer bp_test_… / bp_live_… (서버 전용)

Parameters파라미터

Parameter파라미터In위치Type타입Required필수Description설명
idpathstringYes예Payment link id (plink_…).

Response응답 200 — The link, with product/price summary fields.

{
  "data": {
    "id": "string",
    "share_token": "string",
    "price_id": "string",
    "title": "string",
    "description": "string",
    "collect_email": true,
    "email_required": true,
    "allowed_pay_chains": [
      "string"
    ],
    "allowed_assets": [
      {
        "caip2": "string",
        "token": "string"
      }
    ],
    "success_url": "string",
    "cancel_url": "string",
    "status": "active",
    "max_uses": 0,
    "paid_use_count": 0,
    "expires_at": "2026-11-06T07:00:00.000Z",
    "metadata": {},
    "mode": "TEST",
    "created_at": "2026-11-06T07:00:00.000Z",
    "updated_at": "2026-11-06T07:00:00.000Z"
  },
  "meta": {
    "mode": "TEST",
    "requestId": "string",
    "total": 0,
    "limit": 0,
    "offset": 0
  }
}

Errors:에러: 401 · 404 — see Errors.에러 참고.

PATCH /v1/checkout/payment-links/{id} — Update a payment link

Operational settings and active ↔ inactive. The price cannot change — create a new link instead. Deactivating (status: "inactive") requires reason_code (user_request, security_concern, rotation, no_longer_needed or other, plus an optional reason_note); without it the request returns 400 REASON_CODE_REQUIRED. Reactivating accepts the same fields optionally. Every status change is recorded in your audit log with its reason.

Auth:인증: API key — Authorization: Bearer bp_test_… / bp_live_… (server only)API 키 — Authorization: Bearer bp_test_… / bp_live_… (서버 전용)

Parameters파라미터

Parameter파라미터In위치Type타입Required필수Description설명
idpathstringYes예Payment link id (plink_…).

Request body요청 본문 (UpdatePaymentLinkRequest)

Field필드Type타입Required필수Description설명
titlestring | nullNo아니요1–200 chars
descriptionstring | nullNo아니요≤ 2000 chars
collect_emailbooleanNo아니요
email_requiredbooleanNo아니요
allowed_pay_chainsarray of string | nullNo아니요1–8 items
allowed_assetsarray of object | nullNo아니요1–16 items
success_urlstring (uri) | nullNo아니요≤ 2048 chars
cancel_urlstring (uri) | nullNo아니요≤ 2048 chars
status"active" | "inactive"No아니요
max_usesinteger | nullNo아니요≥ 1
expires_atstring (date-time) | nullNo아니요
metadatamapNo아니요
reason_code"user_request" | "security_concern" | "rotation" | "no_longer_needed" | "other"No아니요
reason_notestringNo아니요≤ 500 chars

Response응답 200 — The updated link.

{
  "data": {
    "id": "string",
    "share_token": "string",
    "price_id": "string",
    "title": "string",
    "description": "string",
    "collect_email": true,
    "email_required": true,
    "allowed_pay_chains": [
      "string"
    ],
    "allowed_assets": [
      {
        "caip2": "string",
        "token": "string"
      }
    ],
    "success_url": "string",
    "cancel_url": "string",
    "status": "active",
    "max_uses": 0,
    "paid_use_count": 0,
    "expires_at": "2026-11-06T07:00:00.000Z",
    "metadata": {},
    "mode": "TEST",
    "created_at": "2026-11-06T07:00:00.000Z",
    "updated_at": "2026-11-06T07:00:00.000Z"
  },
  "meta": {
    "mode": "TEST",
    "requestId": "string",
    "total": 0,
    "limit": 0,
    "offset": 0
  }
}

Errors:에러: 400 · 401 · 403 · 404 · 409 — see Errors.에러 참고.

x402

Charge per API call with HTTP 402 and EIP-3009 authorizations.

GET /v1/x402/assets — List x402 assets for this mode

Auth:인증: API key — Authorization: Bearer bp_test_… / bp_live_… (server only)API 키 — Authorization: Bearer bp_test_… / bp_live_… (서버 전용)

Response응답 200 — Assets.

{
  "data": {
    "mode": "TEST",
    "assets": [
      {
        "caip2": "string",
        "network": "string",
        "chain_name": "string",
        "symbol": "string",
        "decimals": 0,
        "address": "string"
      }
    ]
  },
  "meta": {
    "mode": "TEST",
    "requestId": "string",
    "total": 0,
    "limit": 0,
    "offset": 0
  }
}

Errors:에러: 401 — see Errors.에러 참고.

POST /v1/x402/requirements — Build the 402 payment requirements

Returns the body your resource server sends with 402 Payment Required, including the fee split. Pass exactly one of amount or amountAtomic.

Auth:인증: API key — Authorization: Bearer bp_test_… / bp_live_… (server only)API 키 — Authorization: Bearer bp_test_… / bp_live_… (서버 전용)

Parameters파라미터

Parameter파라미터In위치Type타입Required필수Description설명
Idempotency-KeyheaderstringNo아니요8–128 characters, unique per operation. A retry with the same key and body within 24 hours returns the original response with X-Idempotency-Replayed: true. (8–128 chars)

Request body요청 본문 (X402RequirementsRequest)

Field필드Type타입Required필수Description설명
networkstringNo아니요x402 network name (base, base-sepolia). Defaults to the mode’s first asset. (≥ 1 chars)
assetstringNo아니요Token symbol. Default USDC. (≥ 1 chars, default "USDC")
payTostringYes예Your payout wallet (0x) — must be the one registered in Console for this mode, otherwise PAYTO_NOT_MERCHANT_WALLET.
amountstringNo아니요Gross amount per call as a decimal string, e.g. "0.1". The fee is taken from it.
amountAtomicstringNo아니요Same as amount, as a 6-decimal integer string.
resourcestringYes예The URL being paid for. (1–2048 chars)
descriptionstringNo아니요≤ 512 chars
mimeTypestringNo아니요≤ 128 chars
maxTimeoutSecondsintegerNo아니요How long the payer has to sign (max 3600). Default 300. (≤ 3600, > 0)
{
  "payTo": "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
  "amount": "0.1",
  "resource": "https://api.example.com/report"
}

Response응답 200 — 402 body.

{
  "data": {
    "x402Version": 1,
    "accepts": [
      {
        "scheme": "exact",
        "network": "string",
        "asset": "string",
        "payTo": "string",
        "maxAmountRequired": "1000000",
        "resource": "string",
        "description": "string",
        "mimeType": "string",
        "maxTimeoutSeconds": 0,
        "extra": {
          "name": "string",
          "version": "string",
          "chainId": 0
        },
        "feeBreakdown": {
          "netAmount": "1000000",
          "feeAmount": "1000000",
          "feeBps": 0,
          "feeRecipient": "string"
        }
      }
    ]
  },
  "meta": {
    "mode": "TEST",
    "requestId": "string",
    "total": 0,
    "limit": 0,
    "offset": 0
  }
}

Errors:에러: 400 · 401 · 403 — see Errors.에러 참고.

POST /v1/x402/verify — Verify an X-PAYMENT header

Checks the EIP-3009 authorization against the requirements. Sends nothing on-chain.

Auth:인증: API key — Authorization: Bearer bp_test_… / bp_live_… (server only)API 키 — Authorization: Bearer bp_test_… / bp_live_… (서버 전용)

Parameters파라미터

Parameter파라미터In위치Type타입Required필수Description설명
Idempotency-KeyheaderstringNo아니요8–128 characters, unique per operation. A retry with the same key and body within 24 hours returns the original response with X-Idempotency-Replayed: true. (8–128 chars)

Request body요청 본문 (X402PaymentRequest)

Field필드Type타입Required필수Description설명
x402Version1Yes예
paymentPayloadobjectYes예
paymentRequirementsobjectYes예

Response응답 200 — Valid.

{
  "data": {
    "valid": true,
    "payment_id": "string",
    "status": "string",
    "idempotent": true
  },
  "meta": {
    "mode": "TEST",
    "requestId": "string",
    "total": 0,
    "limit": 0,
    "offset": 0
  }
}

Errors:에러: 400 · 401 · 403 · 409 — see Errors.에러 참고.

POST /v1/x402/settle — Settle an x402 payment

Broadcasts the transfer and waits for the receipt. Serve the resource only when status is confirmed. Settlement is idempotent per authorization — retry with the same body on 5xx.

Auth:인증: API key — Authorization: Bearer bp_test_… / bp_live_… (server only)API 키 — Authorization: Bearer bp_test_… / bp_live_… (서버 전용)

Parameters파라미터

Parameter파라미터In위치Type타입Required필수Description설명
Idempotency-KeyheaderstringNo아니요8–128 characters, unique per operation. A retry with the same key and body within 24 hours returns the original response with X-Idempotency-Replayed: true. (8–128 chars)

Request body요청 본문 (X402PaymentRequest)

Field필드Type타입Required필수Description설명
x402Version1Yes예
paymentPayloadobjectYes예
paymentRequirementsobjectYes예

Response응답 200 — Settlement result.

{
  "data": {
    "success": true,
    "payment_id": "string",
    "status": "string",
    "tx_hash": "string",
    "fee_tx_hash": "string",
    "network": "string",
    "idempotent": true
  },
  "meta": {
    "mode": "TEST",
    "requestId": "string",
    "total": 0,
    "limit": 0,
    "offset": 0
  }
}

Errors:에러: 400 · 401 · 403 · 409 · 503 — see Errors.에러 참고.

GET /v1/x402/payments/{id} — Retrieve an x402 payment

Recovery path when a settle response was lost — check here before settling again.

Auth:인증: API key — Authorization: Bearer bp_test_… / bp_live_… (server only)API 키 — Authorization: Bearer bp_test_… / bp_live_… (서버 전용)

Parameters파라미터

Parameter파라미터In위치Type타입Required필수Description설명
idpathstringYes예Payment id.

Response응답 200 — The payment.

{
  "data": {
    "id": "string",
    "status": "pending",
    "raw_status": "string",
    "resource": "string",
    "chain": "string",
    "asset": "string",
    "decimals": 0,
    "amount": "1000000",
    "gross_amount": "1000000",
    "payer": "string",
    "pay_to": "string",
    "tx_hash": "string",
    "created_at": "2026-11-06T07:00:00.000Z",
    "settled_at": "2026-11-06T07:00:00.000Z"
  },
  "meta": {
    "mode": "TEST",
    "requestId": "string",
    "total": 0,
    "limit": 0,
    "offset": 0
  }
}

Errors:에러: 401 · 404 — see Errors.에러 참고.

GET /v1/x402/payments — List x402 payments

Auth:인증: API key — Authorization: Bearer bp_test_… / bp_live_… (server only)API 키 — Authorization: Bearer bp_test_… / bp_live_… (서버 전용)

Parameters파라미터

Parameter파라미터In위치Type타입Required필수Description설명
limitqueryintegerNo아니요≤ 100, > 0, default 20
offsetqueryintegerNo아니요≥ 0, default 0
statusquerystringNo아니요pending | confirmed | failed | needs_review

Response응답 200 — Payments.

{
  "data": [
    {
      "id": "string",
      "status": "pending",
      "raw_status": "string",
      "resource": "string",
      "chain": "string",
      "asset": "string",
      "decimals": 0,
      "amount": "1000000",
      "gross_amount": "1000000",
      "payer": "string",
      "pay_to": "string",
      "tx_hash": "string",
      "created_at": "2026-11-06T07:00:00.000Z",
      "settled_at": "2026-11-06T07:00:00.000Z"
    }
  ],
  "meta": {
    "mode": "TEST",
    "requestId": "string",
    "total": 0,
    "limit": 0,
    "offset": 0
  }
}

Errors:에러: 400 · 401 — see Errors.에러 참고.

Webhook endpoints

Manage where BitPal delivers signed events.

POST /v1/webhooks — Create a webhook endpoint

**Console only.** Manage this from Console → Developers → Webhooks; calls with an API key return 403. The response carries the whsec_… signing secret — it is shown only once.

Auth:인증: Console login only — manage this in Console → Developers → Webhooks (API keys get 403)콘솔 로그인 전용 — 콘솔 → 개발자 → 웹훅에서 관리 (API 키는 403)

Parameters파라미터

Parameter파라미터In위치Type타입Required필수Description설명
Idempotency-KeyheaderstringNo아니요8–128 characters, unique per operation. A retry with the same key and body within 24 hours returns the original response with X-Idempotency-Replayed: true. (8–128 chars)

Request body요청 본문 (CreateWebhookEndpointRequest)

Field필드Type타입Required필수Description설명
endpoint_urlstring (uri)Yes예HTTPS URL that receives events. Private hosts are rejected.
subscribed_eventsarray of ("*" | "checkout.session.created" | "checkout.session.deposit_detected" | "checkout.session.paid" | "checkout.session.settled" | "checkout.session.expired" | "checkout.session.unresolved" | "checkout.refund.recorded" | "x402.payment.resolved")Yes예Event names to deliver (see the webhooks section), or * for every event. Unknown names are rejected with 400. (≥ 1 items)
retry_policyobjectNo아니요max_retries caps the total delivery attempts (1–20, default 10: 3 right away, then 1 min, 5 min, 30 min, 2 h, 6 h, 12 h, 24 h). The schedule itself is fixed; backoff is ignored.
dead_letter_destinationstring (uri) | nullNo아니요Optional URL for dead-lettered deliveries.
{
  "endpoint_url": "https://your.store/webhooks/bitpal",
  "subscribed_events": [
    "checkout.session.paid"
  ]
}

Response응답 201 — Endpoint created.

{
  "data": {
    "id": "00000000-0000-4000-8000-000000000000",
    "mode": "TEST",
    "endpoint_url": "string",
    "subscribed_events": [
      "string"
    ],
    "retry_policy": {
      "max_retries": 0,
      "backoff": "exponential"
    },
    "dead_letter_destination": "string",
    "status": "active",
    "created_at": "2026-11-06T07:00:00.000Z",
    "updated_at": "2026-11-06T07:00:00.000Z",
    "secret": "string"
  },
  "meta": {
    "mode": "TEST",
    "requestId": "string",
    "total": 0,
    "limit": 0,
    "offset": 0
  }
}

Errors:에러: 400 · 401 — see Errors.에러 참고.

GET /v1/webhooks — List webhook endpoints

**Console only.** Manage this from Console → Developers → Webhooks; calls with an API key return 403. Endpoints of the API key’s environment and mode. Note: list items use camelCase keys.

Auth:인증: Console login only — manage this in Console → Developers → Webhooks (API keys get 403)콘솔 로그인 전용 — 콘솔 → 개발자 → 웹훅에서 관리 (API 키는 403)

Parameters파라미터

Parameter파라미터In위치Type타입Required필수Description설명
limitqueryintegerNo아니요1–100, default 50
offsetqueryintegerNo아니요≥ 0, default 0

Response응답 200 — Endpoints, newest first. meta carries total/limit/offset.

{
  "data": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "mode": "TEST",
      "endpoint_url": "string",
      "subscribed_events": [
        "string"
      ],
      "retry_policy": {
        "max_retries": 0,
        "backoff": "exponential"
      },
      "dead_letter_destination": "string",
      "status": "active",
      "created_at": "2026-11-06T07:00:00.000Z",
      "updated_at": "2026-11-06T07:00:00.000Z"
    }
  ],
  "meta": {
    "mode": "TEST",
    "requestId": "string",
    "total": 0,
    "limit": 0,
    "offset": 0
  }
}

Errors:에러: 401 — see Errors.에러 참고.

PATCH /v1/webhooks/{id} — Update a webhook endpoint

**Console only.** Manage this from Console → Developers → Webhooks; calls with an API key return 403. Setting status to disabled removes the endpoint and requires reason_code (otherwise 400 REASON_CODE_REQUIRED). It is recorded in the audit log.

Auth:인증: Console login only — manage this in Console → Developers → Webhooks (API keys get 403)콘솔 로그인 전용 — 콘솔 → 개발자 → 웹훅에서 관리 (API 키는 403)

Parameters파라미터

Parameter파라미터In위치Type타입Required필수Description설명
idpathstringYes예Webhook endpoint id (uuid).

Request body요청 본문 (UpdateWebhookEndpointRequest)

Field필드Type타입Required필수Description설명
endpoint_urlstring (uri)No아니요
subscribed_eventsarray of ("*" | "checkout.session.created" | "checkout.session.deposit_detected" | "checkout.session.paid" | "checkout.session.settled" | "checkout.session.expired" | "checkout.session.unresolved" | "checkout.refund.recorded" | "x402.payment.resolved")No아니요≥ 1 items
status"active" | "paused" | "disabled"No아니요
retry_policyobjectNo아니요
dead_letter_destinationstring (uri) | nullNo아니요
reason_code"user_request" | "security_concern" | "rotation" | "no_longer_needed" | "other"No아니요Required when status is disabled: one of user_request, security_concern, rotation, no_longer_needed, other.
reason_notestringNo아니요Optional free text (≤ 500 chars) stored with the audit record. (≤ 500 chars)

Response응답 200 — The updated endpoint.

{
  "data": {
    "id": "00000000-0000-4000-8000-000000000000",
    "mode": "TEST",
    "endpoint_url": "string",
    "subscribed_events": [
      "string"
    ],
    "retry_policy": {
      "max_retries": 0,
      "backoff": "exponential"
    },
    "dead_letter_destination": "string",
    "status": "active",
    "created_at": "2026-11-06T07:00:00.000Z",
    "updated_at": "2026-11-06T07:00:00.000Z"
  },
  "meta": {
    "mode": "TEST",
    "requestId": "string",
    "total": 0,
    "limit": 0,
    "offset": 0
  }
}

Errors:에러: 400 · 401 · 404 — see Errors.에러 참고.

POST /v1/webhooks/{id}/rotate-secret — Rotate the signing secret

**Console only.** Manage this from Console → Developers → Webhooks; calls with an API key return 403. Takes effect immediately. The new secret is returned once — update your verifier. Requires reason_code (400 REASON_CODE_REQUIRED if missing); recorded in the audit log.

Auth:인증: Console login only — manage this in Console → Developers → Webhooks (API keys get 403)콘솔 로그인 전용 — 콘솔 → 개발자 → 웹훅에서 관리 (API 키는 403)

Parameters파라미터

Parameter파라미터In위치Type타입Required필수Description설명
idpathstringYes예Webhook endpoint id (uuid).
Idempotency-KeyheaderstringNo아니요8–128 characters, unique per operation. A retry with the same key and body within 24 hours returns the original response with X-Idempotency-Replayed: true. (8–128 chars)

Request body요청 본문 (RotateWebhookSecretRequest)

Field필드Type타입Required필수Description설명
reason_code"user_request" | "security_concern" | "rotation" | "no_longer_needed" | "other"Yes예One of user_request, security_concern, rotation, no_longer_needed, other.
reason_notestringNo아니요Optional free text (≤ 500 chars) stored with the audit record. (≤ 500 chars)
{
  "reason_code": "rotation"
}

Response응답 200 — Endpoint with the new secret.

{
  "data": {
    "id": "00000000-0000-4000-8000-000000000000",
    "mode": "TEST",
    "endpoint_url": "string",
    "subscribed_events": [
      "string"
    ],
    "retry_policy": {
      "max_retries": 0,
      "backoff": "exponential"
    },
    "dead_letter_destination": "string",
    "status": "active",
    "created_at": "2026-11-06T07:00:00.000Z",
    "updated_at": "2026-11-06T07:00:00.000Z",
    "secret": "string"
  },
  "meta": {
    "mode": "TEST",
    "requestId": "string",
    "total": 0,
    "limit": 0,
    "offset": 0
  }
}

Errors:에러: 400 · 401 · 404 — see Errors.에러 참고.

POST /v1/developers/webhooks/test — Send a test event

Posts one signed sample event to an endpoint (no retries). The envelope id is prefixed test_, data.test is true and the request carries X-Webhook-Test: true.

Auth:인증: API key — Authorization: Bearer bp_test_… / bp_live_… (server only)API 키 — Authorization: Bearer bp_test_… / bp_live_… (서버 전용)

Parameters파라미터

Parameter파라미터In위치Type타입Required필수Description설명
Idempotency-KeyheaderstringNo아니요8–128 characters, unique per operation. A retry with the same key and body within 24 hours returns the original response with X-Idempotency-Replayed: true. (8–128 chars)

Request body요청 본문 (SendTestWebhookRequest)

Field필드Type타입Required필수Description설명
webhook_idstring (uuid)Yes예Endpoint id.
event_typestringYes예Event name to put in the envelope, e.g. checkout.session.paid. (1–200 chars)
payloadmapNo아니요Custom data object. Defaults to a sample message.
{
  "webhook_id": "00000000-0000-4000-8000-000000000000",
  "event_type": "checkout.session.paid"
}

Response응답 200 — Delivery result.

{
  "data": {
    "delivery_id": "00000000-0000-4000-8000-000000000000",
    "webhook_id": "00000000-0000-4000-8000-000000000000",
    "endpoint_url": "string",
    "event_type": "string",
    "http_status": 0,
    "response_body": "string",
    "error": "string",
    "duration_ms": 0,
    "test": true,
    "sent_at": "2026-11-06T07:00:00.000Z"
  },
  "meta": {
    "mode": "TEST",
    "requestId": "string",
    "total": 0,
    "limit": 0,
    "offset": 0
  }
}

Errors:에러: 400 · 401 · 404 · 409 — see Errors.에러 참고.

Webhook events웹훅 이벤트

Envelope: { id, event, created_at, data }, signed with X-Webhook-Signature-256. Verification and retries: Webhooks.

본문 구조: { id, event, created_at, data }, X-Webhook-Signature-256으로 서명됩니다. 검증과 재시도는 웹훅을 참고하세요.

Event이벤트Fires when발생 시점data fieldsdata 필드
checkout.session.createdA session is created.session_id, metadata, amount_total, currency, late
checkout.session.deposit_detectedFunds seen on-chain, awaiting confirmations. Optional — skipped when the first time BitPal sees the deposit it is already confirmed; then checkout.session.paid (or unresolved) arrives with no deposit_detected before it. Do not wait for it.session_id, metadata, amount_total, currency, late, tx_hash, amount_received, received_token
checkout.session.paidConfirmed — safe to fulfil. late: true if it arrived after the deadline.session_id, metadata, amount_total, currency, late, tx_hash
checkout.session.settledFunds swept to your payout wallet.session_id, metadata, amount_total, currency, late, sweep_tx_hash
checkout.session.unresolvedUnderpaid, wrong token, or a confirmed deposit reverted by a reorg (reason).session_id, metadata, amount_total, currency, late, reason, amount_expected, amount_received, received_token, tx_hash
checkout.session.expiredDeadline passed without full payment.session_id, metadata, amount_total, currency, late, reason, expired_at, source, refund_tx_hash
checkout.refund.recordedA refund was made on-chain (kind): automatic, or recorded by you in the console (merchant).session_id, metadata, kind, amount, token, tx_hash, refund_address, from_address, reason_code
x402.payment.resolvedAn x402 call that settled with 503 reached its outcome. Only fires for those — the normal path answers inline.payment_id, status, resource, amount, asset, chain, tx_hash, payer, resolved_by