@bitpal/checkout
v0.3.0
Published
BitPal Checkout SDK — crypto payment sessions, external on-chain payment verification, gasless checkout (with PaymentAuthorization double-defense), refunds, fee preview.
Downloads
173
Maintainers
Readme
@bitpal/checkout
BitPal Checkout SDK — 크립토 결제 세션 생성, 외부 on-chain 결제 검증, gasless 결제, 환불, 수수료 미리보기를 위한 공식 TypeScript SDK.
설치
pnpm add @bitpal/checkout⚠️ 금액 형식 (꼭 읽어주세요)
BitPal API는 모든 amount를 atomic μUSDC 정수 문자열로 받습니다 (USDC/USDT 6 decimals).
| 의미 | 보내는 값 |
|------|-----------|
| $1.00 | "1000000" |
| $10.00 | "10000000" |
| $29.00 | "29000000" |
| $0.50 | "500000" |
"29.00"처럼 decimal 표기로 보내면 400 에러가 떨어집니다. SDK가 제공하는 toAtomicUSDC() 헬퍼를 쓰세요:
import { toAtomicUSDC, fromAtomicUSDC } from '@bitpal/checkout';
toAtomicUSDC('29.00'); // → '29000000'
toAtomicUSDC('0.5'); // → '500000'
fromAtomicUSDC('1500000'); // → '1.50'이 정책은 부동소수 정밀도 손실 방지 때문이며, 모든 BitPal 라우트에 일관 적용됩니다.
환경 (Environments)
BitPal Checkout 은 두 개의 직교 (orthogonal) 축으로 환경이 결정됩니다.
| 축 | 값 | 결정 방식 | 영향 |
|----|----|---------|------|
| mode | TEST / LIVE | API 키 prefix (bp_test_* / bp_live_*) | 어떤 컨트랙트 / 정산 / webhook environment 가 사용될지 |
| API host | production / local | BitPal({ baseUrl }) | 어떤 BitPal 백엔드를 호출할지 (실서비스 vs 본인 dev 머신) |
조합 매트릭스:
| 시나리오 | baseUrl | API 키 | 실제 체인 |
|---------|-----------|--------|----------|
| Local 개발 (anvil) | http://localhost:3100 | bp_test_* | 로컬 Hardhat/Anvil 체인 (http://127.0.0.1:8545) |
| Testnet 통합 | https://api.bitpal.io | bp_test_* | Base Sepolia 등 퍼블릭 testnet |
| Production | https://api.bitpal.io | bp_live_* | Base Mainnet |
import { BitPal } from '@bitpal/checkout';
// 1) Production / testnet (BitPal 호스티드 백엔드)
const bitpal = new BitPal({
apiKey: process.env.BITPAL_API_KEY!, // bp_test_* (testnet) 또는 bp_live_* (mainnet)
});
// 2) Local 개발 (본인 머신의 BitPal API + anvil)
const bitpalLocal = new BitPal({
apiKey: process.env.BITPAL_API_KEY!, // bp_test_* 필수
baseUrl: 'http://localhost:3100',
});어떤 체인이든 SDK 호출 시점에
chain: 'base'(또는 다른 지원 체인) 만 지정하면 됩니다. mode 가 mainnet/testnet/local 컨트랙트 주소 매핑을 결정하므로 클라이언트는 체인 이름 만 알면 충분합니다.
사용법
기본 설정
import { BitPal } from '@bitpal/checkout';
// API 키는 BitPal 콘솔 → Developers → API Keys 에서 발급
const bitpal = new BitPal({ apiKey: process.env.BITPAL_API_KEY! });체크아웃 세션 생성
import { BitPal, toAtomicUSDC } from '@bitpal/checkout';
const bitpal = new BitPal({ apiKey: process.env.BITPAL_API_KEY! });
const { data: session } = await bitpal.checkout.createSession({
line_items: [
{ name: 'Pro Plan', amount: toAtomicUSDC('29.00'), currency: 'USDC' },
],
pay_chain: 'eip155:8453', // Base
expires_in_seconds: 1800,
success_url: 'https://yoursite.com/order-complete',
cancel_url: 'https://yoursite.com/cart',
});
// buyer를 이 URL로 redirect → BitPal이 호스팅하는 결제 페이지
console.log(session.url);환불 (auto — 에스크로 보관 중)
import { randomUUID } from 'node:crypto';
import { BitPal, toAtomicUSDC } from '@bitpal/checkout';
const bitpal = new BitPal(process.env.BITPAL_API_KEY!);
// 전액 환불 — amount 미지정
const { data: full } = await bitpal.checkout.refundSession(
sessionId,
{ reason: 'customer_requested' },
{ idempotencyKey: randomUUID() }, // 동일 키 재시도 시 같은 결과 반환
);
console.log(full.refunded, full.txHash); // true, '0x...' (또는 sim_xxx)
// 부분 환불 — amount 명시
const { data: partial } = await bitpal.checkout.refundSession(
sessionId,
{ amount: toAtomicUSDC('5.00'), reason: 'partial_damage' },
{ idempotencyKey: randomUUID() },
);
console.log(partial.type); // 'partial'머천트가 frozen 상태면 환불은 허용되지만, suspended 상태에서는 차단된다 (merchant-status-guard).
환불 기록 (record — settled 후 manual offchain)
// 정산이 끝난 세션은 escrow에 자금이 없음 → 머천트가 외부 지갑에서 직접 송금하고
// tx_hash를 입력해 환불을 DB-only로 기록한다.
await bitpal.checkout.recordRefund(
sessionId,
{
refund_amount: toAtomicUSDC('5.00'),
tx_hash: '0x...실제 송금 tx hash',
reason: 'manual_offchain_refund',
},
{ idempotencyKey: randomUUID() },
);환불 내역 조회
const { data: refunds } = await bitpal.checkout.listRefunds(sessionId);
// auto + record 통합 list, 시간 역순수수료 미리보기
// $100 결제의 수수료 계산
const { data: fee } = await bitpal.checkout.previewFee(toAtomicUSDC('100'));
// fee.fee_amount === "1500000" (1.5% = $1.50)
// fee.net_amount === "98500000" (= $98.50)
console.log(`수수료: $${fromAtomicUSDC(fee.fee_amount)}`);Gasless external checkout payment
Use payWithAuthorization() when your merchant UI collects an EIP-3009 USDC authorization from the buyer and wants BitPal to submit the escrow deposit with the platform relayer.
const { data: session } = await bitpal.checkout.getSession(sessionId);
const cfg = session.external_payment_config;
// Sign EIP-3009 typed data in your wallet flow using cfg.token_address,
// cfg.escrow_contract_address, cfg.amount, and cfg.payment_id.
const authorization = {
from: '0xBuyer',
validAfter: '0',
validBefore: '1778650000',
authNonce: '0x...',
v: 27,
r: '0x...',
s: '0x...',
};
const { data: payment } = await bitpal.checkout.payWithAuthorization(sessionId, {
chain: cfg?.chain ?? 'base',
session_token: session.session_token!,
authorization,
});
console.log(payment.status, payment.tx_hash);payWithAuthorization() calls POST /v1/checkout/sessions/:id/submit-payment. The buyer signs only the authorization; BitPal's relayer submits CheckoutEscrow.depositWithAuthorization() and pays gas.
SDK 의 반환값(status, tx_hash) 만으로 merchant 백엔드가 주문을 finalize 할 수 있습니다. webhook 등록은 선택 — SDK 호출 없이 자동 알림이 필요한 머천트 백엔드 (비동기 잡, 다른 마이크로서비스 fan-out 등) 만 등록하면 됩니다.
External on-chain payment verification (submitExternalPayment)
When the buyer deposits into CheckoutEscrow from their own wallet, merchant UI, or your own backend — outside the BitPal hosted page and outside payWithAuthorization() — submit the resulting tx_hash to BitPal for verification.
const { data: result } = await bitpal.checkout.submitExternalPayment(sessionId, {
tx_hash: '0xb81f1eb2cc8a4b32a233865c595166df563f8f009e319048f8d079abd0f3953d',
chain: 'base',
});
// result.validation_status: 'valid' | 'invalid' | 'underpaid' | 'overpaid' | 'duplicate'
// result.session_status: 'paid' | 'unresolved_underpaid' | 'unresolved_overpaid' | ...
// result.observation_id: uuid of the checkout_payment_observations row
// result.from_address: payer wallet (derived from PaymentDeposited event)
// result.amount: atomic μUSDC received (derived from event)Behaviour:
- BitPal fetches the receipt, parses
CheckoutEscrow.PaymentDeposited, and verifiespaymentId == toBytes32(sessionId), merchant wallet, USDC token, and amount tolerance. - The same
(chain_id, tx_hash, log_index)is rejected withvalidation_status: 'duplicate'if it has already been submitted for a different session. - Re-submitting the same
tx_hashfor the same session is idempotent — you get the same observation row back. - If you're using the Ponder Mode B indexer (
apps/indexer) the indexer calls this endpoint automatically when it sees aPaymentDepositedevent, so you do not need to submit manually.
// Read the on-chain verification history for a session
const { data: observations } = await bitpal.checkout.getExternalPaymentStatus(sessionId);
const latest = observations[0];
if (latest?.finality_status === 'final_confirmed') {
// 12 confirmations reached — safe to fulfil orders that require finality
}getExternalPaymentStatus() is the authenticated read counterpart for tenant-scoped clients. It returns the same payment_observations array that the merchant console session detail page shows under "On-chain Verification".
Choosing between SDK payment methods
| Method | Buyer signs | Who calls CheckoutEscrow.deposit* | Use when |
|--------|------------|--------------------------------------|----------|
| Hosted checkout (createSession → redirect to session.url) | EIP-3009 inside BitPal page | BitPal relayer | You want the simplest integration and don't need a buyer UI |
| payWithAuthorization() | EIP-3009 inside your UI | BitPal relayer | Buyer stays in merchant UI, gasless for buyer |
| submitExternalPayment() | Buyer signs the deposit tx in their wallet | Buyer wallet or your backend | Buyer pays their own gas, or you have a non-BitPal flow that emits PaymentDeposited |
All three paths land in the same CheckoutEscrow.PaymentDeposited event and trigger the same set of webhooks (checkout.session.paid, checkout.session.unresolved, checkout.payment.finalized, checkout.payment.revoked).
웹훅 검증
import express from 'express';
import { verifyWebhookSignature, parseWebhookEvent, WEBHOOK_HEADERS } from '@bitpal/checkout';
const app = express();
// raw body 필수 — express.json() 쓰지 말 것
app.post('/webhook', express.raw({ type: 'application/json' }), async (req, res) => {
const signature = req.header(WEBHOOK_HEADERS.signature) ?? ''; // 'X-Webhook-Signature-256'
const isValid = await verifyWebhookSignature(
req.body.toString(),
signature,
process.env.BITPAL_WEBHOOK_SECRET!,
);
if (!isValid) return res.status(400).send('invalid signature');
const event = parseWebhookEvent(req.body.toString());
switch (event.event) {
case 'checkout.session.paid':
// DB에 주문 완료 처리 (idempotent — at-least-once 전달)
break;
case 'checkout.payment.failed':
// 실패 처리
break;
}
res.status(200).send('ok');
});Webhook 헤더 (canonical)
BitPal backend가 모든 webhook delivery에 보내는 표준 헤더:
| 헤더 | 값 | 용도 |
|------|----|------|
| X-Webhook-Signature-256 | sha256=<hex> | HMAC-SHA256 서명 (raw body 기준) |
| X-Webhook-Timestamp | ISO 시각 | envelope.created_at과 동일 |
| X-Webhook-Event | checkout.session.paid 등 | canonical event name |
| X-Webhook-Id | evt_... | event_id (replay 시 보존) |
| X-Webhook-Delivery-Id | dlv_... | delivery row id |
WEBHOOK_HEADERS 상수로 SDK에서 import 가능 — 헤더 이름 drift 방지.
⚠️ At-least-once delivery — 머천트 idempotency 책임
BitPal webhook은 at-least-once delivery다. 같은 이벤트가 재시도/replay/race로 인해 두 번 이상 도달할 수 있으며, 머천트 측에서 반드시 idempotent하게 처리해야 한다.
필수 패턴:
- signature 검증 후
X-Webhook-Id(또는X-Webhook-Delivery-Id) 기준으로 dedup- 이미 처리한 event는 200 OK만 반환하고 부작용 없이 종료
import { verifyWebhookSignature, parseWebhookEvent, WEBHOOK_HEADERS } from '@bitpal/checkout';
app.post('/webhook', express.raw({ type: 'application/json' }), async (req, res) => {
// 1. signature 검증
const sig = req.header(WEBHOOK_HEADERS.signature) ?? '';
const ok = await verifyWebhookSignature(req.body.toString(), sig, SECRET);
if (!ok) return res.status(400).send('invalid signature');
// 2. dedup — X-Webhook-Id 기준
const eventId = req.header(WEBHOOK_HEADERS.eventId);
const seen = await db.webhook_events.findUnique({ where: { event_id: eventId } });
if (seen) return res.status(200).send('already processed');
// 3. event 처리 + 처리 기록을 동일 트랜잭션에서
const event = parseWebhookEvent(req.body.toString());
await db.$transaction(async (tx) => {
await applyEvent(tx, event); // 비즈니스 로직
await tx.webhook_events.create({ data: { event_id: eventId } }); // dedup 키 기록
});
res.status(200).send('ok');
});dedup 키로 X-Webhook-Id (event 단위) 또는 X-Webhook-Delivery-Id (delivery 단위) 둘 다 가능 — 이벤트 단위 dedup이 일반적으로 더 안전하다 (replay도 같은 event_id 보존).
설정 옵션
| 옵션 | 타입 | 기본값 | 설명 |
|------|------|--------|------|
| apiKey | string | (필수) | API 키 (bp_test_... 또는 bp_live_...) |
| baseUrl | string | https://api.bitpal.io | API 기본 URL (로컬: http://localhost:3100) |
| timeout | number | 30000 | 요청 타임아웃 (ms) |
웹훅 이벤트
SDK CHECKOUT_WEBHOOK_EVENTS 상수와 1:1 정합. 새 이벤트 추가 시 backend와 동시 갱신.
| 이벤트 | 설명 |
|--------|------|
| checkout.session.created | 세션 생성됨 |
| checkout.session.paid | 결제 완료 (에스크로 락) |
| checkout.session.settled | 정산 완료 (에스크로 해제) |
| checkout.session.expired | 세션 만료 |
| checkout.session.unresolved | 세션 미해결 (수동 정정 필요) |
| checkout.session.release_deferred | 머천트 frozen/suspended로 release 보류 |
| checkout.payment.failed | 결제 실패 |
| checkout.payment.finalized | 외부 온체인 결제 최종 확인 |
| checkout.payment.revoked | 외부 온체인 결제 reorg 취소 |
| checkout.refund.recorded | 환불 기록 (settled 후 manual) |
| checkout.refund.completed | 환불 완료 (전액) |
| checkout.refund.partial | 부분 환불 |
통합 흐름
머천트 백엔드 BitPal API Buyer
│ │ │
│ 1. createSession() │ │
│ ──────────────────────────► │ │
│ 2. { url } 응답 │ │
│ ◄──────────────────────────┤ │
│ │ │
│ 3. buyer를 url로 redirect ───────────────────► │
│ │ │
│ │ 4. 결제 진행 │
│ │ ◄────────────────┤
│ │ │
│ 5. POST /merchant/webhook │ │
│ ◄─── checkout.session.paid ─┤ │
│ 6. signature 검증 │ │
│ 7. DB에 주문 완료 처리 │ │실행 가능한 예제
examples/ 디렉토리에 통합 데모가 들어있습니다:
examples/create-session.mjs— 세션 생성 → 결제 URL 출력examples/webhook-receiver.mjs— webhook 수신 + 서명 검증
cd examples
node create-session.mjs빌드
pnpm --filter @bitpal/checkout build라이선스
Private
