@bitpal/checkout 0.2.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +221 -392
- package/dist/index.cjs +374 -0
- package/dist/index.d.cts +566 -0
- package/dist/index.d.ts +266 -411
- package/dist/index.js +118 -201
- package/package.json +32 -16
- package/MIGRATION_v0.2.md +0 -135
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 BitPal
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,392 +1,221 @@
|
|
|
1
|
-
# @bitpal/checkout
|
|
2
|
-
|
|
3
|
-
BitPal Checkout SDK —
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
toAtomicUSDC
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
import { BitPal }
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
-
|
|
216
|
-
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
const { data: observations } = await bitpal.checkout.getExternalPaymentStatus(sessionId);
|
|
223
|
-
const latest = observations[0];
|
|
224
|
-
if (latest?.finality_status === 'final_confirmed') {
|
|
225
|
-
// 12 confirmations reached — safe to fulfil orders that require finality
|
|
226
|
-
}
|
|
227
|
-
```
|
|
228
|
-
|
|
229
|
-
`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".
|
|
230
|
-
|
|
231
|
-
#### Choosing between SDK payment methods
|
|
232
|
-
|
|
233
|
-
| Method | Buyer signs | Who calls `CheckoutEscrow.deposit*` | Use when |
|
|
234
|
-
|--------|------------|--------------------------------------|----------|
|
|
235
|
-
| 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 |
|
|
236
|
-
| `payWithAuthorization()` | EIP-3009 inside your UI | BitPal relayer | Buyer stays in merchant UI, gasless for buyer |
|
|
237
|
-
| `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` |
|
|
238
|
-
|
|
239
|
-
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`).
|
|
240
|
-
|
|
241
|
-
### 웹훅 검증
|
|
242
|
-
|
|
243
|
-
```ts
|
|
244
|
-
import express from 'express';
|
|
245
|
-
import { verifyWebhookSignature, parseWebhookEvent, WEBHOOK_HEADERS } from '@bitpal/checkout';
|
|
246
|
-
|
|
247
|
-
const app = express();
|
|
248
|
-
|
|
249
|
-
// raw body 필수 — express.json() 쓰지 말 것
|
|
250
|
-
app.post('/webhook', express.raw({ type: 'application/json' }), async (req, res) => {
|
|
251
|
-
const signature = req.header(WEBHOOK_HEADERS.signature) ?? ''; // 'X-Webhook-Signature-256'
|
|
252
|
-
const isValid = await verifyWebhookSignature(
|
|
253
|
-
req.body.toString(),
|
|
254
|
-
signature,
|
|
255
|
-
process.env.BITPAL_WEBHOOK_SECRET!,
|
|
256
|
-
);
|
|
257
|
-
if (!isValid) return res.status(400).send('invalid signature');
|
|
258
|
-
|
|
259
|
-
const event = parseWebhookEvent(req.body.toString());
|
|
260
|
-
switch (event.event) {
|
|
261
|
-
case 'checkout.session.paid':
|
|
262
|
-
// DB에 주문 완료 처리 (idempotent — at-least-once 전달)
|
|
263
|
-
break;
|
|
264
|
-
case 'checkout.payment.failed':
|
|
265
|
-
// 실패 처리
|
|
266
|
-
break;
|
|
267
|
-
}
|
|
268
|
-
res.status(200).send('ok');
|
|
269
|
-
});
|
|
270
|
-
```
|
|
271
|
-
|
|
272
|
-
#### Webhook 헤더 (canonical)
|
|
273
|
-
|
|
274
|
-
BitPal backend가 모든 webhook delivery에 보내는 표준 헤더:
|
|
275
|
-
|
|
276
|
-
| 헤더 | 값 | 용도 |
|
|
277
|
-
|------|----|------|
|
|
278
|
-
| `X-Webhook-Signature-256` | `sha256=<hex>` | HMAC-SHA256 서명 (raw body 기준) |
|
|
279
|
-
| `X-Webhook-Timestamp` | ISO 시각 | envelope.created_at과 동일 |
|
|
280
|
-
| `X-Webhook-Event` | `checkout.session.paid` 등 | canonical event name |
|
|
281
|
-
| `X-Webhook-Id` | `evt_...` | event_id (replay 시 보존) |
|
|
282
|
-
| `X-Webhook-Delivery-Id` | `dlv_...` | delivery row id |
|
|
283
|
-
|
|
284
|
-
`WEBHOOK_HEADERS` 상수로 SDK에서 import 가능 — 헤더 이름 drift 방지.
|
|
285
|
-
|
|
286
|
-
### ⚠️ At-least-once delivery — 머천트 idempotency 책임
|
|
287
|
-
|
|
288
|
-
BitPal webhook은 **at-least-once delivery**다. 같은 이벤트가 재시도/replay/race로 인해 두 번 이상 도달할 수 있으며, 머천트 측에서 반드시 idempotent하게 처리해야 한다.
|
|
289
|
-
|
|
290
|
-
**필수 패턴**:
|
|
291
|
-
1. signature 검증 후
|
|
292
|
-
2. `X-Webhook-Id` (또는 `X-Webhook-Delivery-Id`) 기준으로 dedup
|
|
293
|
-
3. 이미 처리한 event는 200 OK만 반환하고 부작용 없이 종료
|
|
294
|
-
|
|
295
|
-
```ts
|
|
296
|
-
import { verifyWebhookSignature, parseWebhookEvent, WEBHOOK_HEADERS } from '@bitpal/checkout';
|
|
297
|
-
|
|
298
|
-
app.post('/webhook', express.raw({ type: 'application/json' }), async (req, res) => {
|
|
299
|
-
// 1. signature 검증
|
|
300
|
-
const sig = req.header(WEBHOOK_HEADERS.signature) ?? '';
|
|
301
|
-
const ok = await verifyWebhookSignature(req.body.toString(), sig, SECRET);
|
|
302
|
-
if (!ok) return res.status(400).send('invalid signature');
|
|
303
|
-
|
|
304
|
-
// 2. dedup — X-Webhook-Id 기준
|
|
305
|
-
const eventId = req.header(WEBHOOK_HEADERS.eventId);
|
|
306
|
-
const seen = await db.webhook_events.findUnique({ where: { event_id: eventId } });
|
|
307
|
-
if (seen) return res.status(200).send('already processed');
|
|
308
|
-
|
|
309
|
-
// 3. event 처리 + 처리 기록을 동일 트랜잭션에서
|
|
310
|
-
const event = parseWebhookEvent(req.body.toString());
|
|
311
|
-
await db.$transaction(async (tx) => {
|
|
312
|
-
await applyEvent(tx, event); // 비즈니스 로직
|
|
313
|
-
await tx.webhook_events.create({ data: { event_id: eventId } }); // dedup 키 기록
|
|
314
|
-
});
|
|
315
|
-
|
|
316
|
-
res.status(200).send('ok');
|
|
317
|
-
});
|
|
318
|
-
```
|
|
319
|
-
|
|
320
|
-
dedup 키로 `X-Webhook-Id` (event 단위) 또는 `X-Webhook-Delivery-Id` (delivery 단위) 둘 다 가능 — 이벤트 단위 dedup이 일반적으로 더 안전하다 (replay도 같은 event_id 보존).
|
|
321
|
-
|
|
322
|
-
---
|
|
323
|
-
|
|
324
|
-
## 설정 옵션
|
|
325
|
-
|
|
326
|
-
| 옵션 | 타입 | 기본값 | 설명 |
|
|
327
|
-
|------|------|--------|------|
|
|
328
|
-
| `apiKey` | `string` | (필수) | API 키 (`bp_test_...` 또는 `bp_live_...`) |
|
|
329
|
-
| `baseUrl` | `string` | `https://api.bitpal.io` | API 기본 URL (로컬: `http://localhost:3100`) |
|
|
330
|
-
| `timeout` | `number` | `30000` | 요청 타임아웃 (ms) |
|
|
331
|
-
|
|
332
|
-
## 웹훅 이벤트
|
|
333
|
-
|
|
334
|
-
SDK `CHECKOUT_WEBHOOK_EVENTS` 상수와 1:1 정합. 새 이벤트 추가 시 backend와 동시 갱신.
|
|
335
|
-
|
|
336
|
-
| 이벤트 | 설명 |
|
|
337
|
-
|--------|------|
|
|
338
|
-
| `checkout.session.created` | 세션 생성됨 |
|
|
339
|
-
| `checkout.session.paid` | 결제 완료 (에스크로 락) |
|
|
340
|
-
| `checkout.session.settled` | 정산 완료 (에스크로 해제) |
|
|
341
|
-
| `checkout.session.expired` | 세션 만료 |
|
|
342
|
-
| `checkout.session.unresolved` | 세션 미해결 (수동 정정 필요) |
|
|
343
|
-
| `checkout.session.release_deferred` | 머천트 frozen/suspended로 release 보류 |
|
|
344
|
-
| `checkout.payment.failed` | 결제 실패 |
|
|
345
|
-
| `checkout.payment.finalized` | 외부 온체인 결제 최종 확인 |
|
|
346
|
-
| `checkout.payment.revoked` | 외부 온체인 결제 reorg 취소 |
|
|
347
|
-
| `checkout.refund.recorded` | 환불 기록 (settled 후 manual) |
|
|
348
|
-
| `checkout.refund.completed` | 환불 완료 (전액) |
|
|
349
|
-
| `checkout.refund.partial` | 부분 환불 |
|
|
350
|
-
|
|
351
|
-
## 통합 흐름
|
|
352
|
-
|
|
353
|
-
```
|
|
354
|
-
머천트 백엔드 BitPal API Buyer
|
|
355
|
-
│ │ │
|
|
356
|
-
│ 1. createSession() │ │
|
|
357
|
-
│ ──────────────────────────► │ │
|
|
358
|
-
│ 2. { url } 응답 │ │
|
|
359
|
-
│ ◄──────────────────────────┤ │
|
|
360
|
-
│ │ │
|
|
361
|
-
│ 3. buyer를 url로 redirect ───────────────────► │
|
|
362
|
-
│ │ │
|
|
363
|
-
│ │ 4. 결제 진행 │
|
|
364
|
-
│ │ ◄────────────────┤
|
|
365
|
-
│ │ │
|
|
366
|
-
│ 5. POST /merchant/webhook │ │
|
|
367
|
-
│ ◄─── checkout.session.paid ─┤ │
|
|
368
|
-
│ 6. signature 검증 │ │
|
|
369
|
-
│ 7. DB에 주문 완료 처리 │ │
|
|
370
|
-
```
|
|
371
|
-
|
|
372
|
-
## 실행 가능한 예제
|
|
373
|
-
|
|
374
|
-
`examples/` 디렉토리에 통합 데모가 들어있습니다:
|
|
375
|
-
|
|
376
|
-
- `examples/create-session.mjs` — 세션 생성 → 결제 URL 출력
|
|
377
|
-
- `examples/webhook-receiver.mjs` — webhook 수신 + 서명 검증
|
|
378
|
-
|
|
379
|
-
```bash
|
|
380
|
-
cd examples
|
|
381
|
-
node create-session.mjs
|
|
382
|
-
```
|
|
383
|
-
|
|
384
|
-
## 빌드
|
|
385
|
-
|
|
386
|
-
```bash
|
|
387
|
-
pnpm --filter @bitpal/checkout build
|
|
388
|
-
```
|
|
389
|
-
|
|
390
|
-
## 라이선스
|
|
391
|
-
|
|
392
|
-
Private
|
|
1
|
+
# @bitpal/checkout
|
|
2
|
+
|
|
3
|
+
BitPal Checkout SDK — **non-custodial** crypto checkout. Create sessions, issue per-order deposit
|
|
4
|
+
addresses, poll status, and verify signed webhooks. BitPal never custodies funds: each order gets a
|
|
5
|
+
deposit address cryptographically committed to your wallet, and payments are swept directly to you.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pnpm add @bitpal/checkout
|
|
11
|
+
# npm i @bitpal/checkout / yarn add @bitpal/checkout
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## ⚠️ Amount format (read this first)
|
|
15
|
+
|
|
16
|
+
USDC / USDT have **6 decimals**. All amounts are **atomic μUSDC integer strings** — never floats.
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
$1.00 → "1000000"
|
|
20
|
+
$29.00 → "29000000"
|
|
21
|
+
$0.50 → "500000"
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Passing a decimal string like `"29.00"` returns **400**. Use the helpers:
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import { toAtomicUSDC, fromAtomicUSDC } from '@bitpal/checkout';
|
|
28
|
+
|
|
29
|
+
toAtomicUSDC('29.00'); // → "29000000"
|
|
30
|
+
fromAtomicUSDC('29000000'); // → "29.00"
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Environments
|
|
34
|
+
|
|
35
|
+
API keys are environment-scoped:
|
|
36
|
+
|
|
37
|
+
- `bp_test_...` — Test mode (testnets). No real funds.
|
|
38
|
+
- `bp_live_...` — Live mode (mainnets).
|
|
39
|
+
|
|
40
|
+
Get keys in the console under **Developers → API Keys**. The key you use decides the environment —
|
|
41
|
+
there is no separate flag.
|
|
42
|
+
|
|
43
|
+
## Usage
|
|
44
|
+
|
|
45
|
+
### Setup
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
// helpers (toAtomicUSDC / fromAtomicUSDC) are used in the snippets below
|
|
49
|
+
import { BitPal, toAtomicUSDC, fromAtomicUSDC } from '@bitpal/checkout';
|
|
50
|
+
|
|
51
|
+
const bitpal = new BitPal(process.env.BITPAL_API_KEY!); // bp_test_... / bp_live_...
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
> Works with both ESM (`import`) and CommonJS (`const { BitPal } = require('@bitpal/checkout')`).
|
|
55
|
+
|
|
56
|
+
### Create a session → redirect to the hosted checkout (simplest)
|
|
57
|
+
|
|
58
|
+
`createSession` returns a `checkout_url`. Redirect the buyer there — BitPal hosts the payment page,
|
|
59
|
+
shows the per-order deposit address + QR, and confirms the payment on-chain.
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
const { data: session } = await bitpal.checkout.createSession({
|
|
63
|
+
line_items: [
|
|
64
|
+
{ name: 'Pro Plan', amount: toAtomicUSDC('29.00'), currency: 'USDC' },
|
|
65
|
+
],
|
|
66
|
+
pay_chain: 'eip155:84532', // Base Sepolia (test mode). Live mode: 'eip155:8453' (Base mainnet).
|
|
67
|
+
// Or use allowed_pay_chains for a buyer-picked chain.
|
|
68
|
+
expires_in_seconds: 1800,
|
|
69
|
+
success_url: 'https://yourstore.com/thanks',
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Redirect the buyer here — the hosted page handles the deposit address.
|
|
73
|
+
redirect(session.checkout_url!);
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
`pay_chain` (single fixed chain) and `allowed_pay_chains` (buyer picks) are mutually exclusive.
|
|
77
|
+
|
|
78
|
+
### Self-hosted flow: issue a deposit address yourself
|
|
79
|
+
|
|
80
|
+
If you render your own payment UI, issue the per-order deposit address and show it to the buyer.
|
|
81
|
+
The buyer then sends the exact amount from **any wallet or exchange** — no wallet connection, no
|
|
82
|
+
signature; they pay their own network gas. A watcher detects the deposit and a keeper sweeps it to
|
|
83
|
+
you.
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
const { data } = await bitpal.checkout.issueDepositAddress(session.id, {
|
|
87
|
+
session_token: session.session_token!, // from createSession
|
|
88
|
+
refundAddress: '0xBuyerWallet...', // committed to the address, immutable after issuance
|
|
89
|
+
chain: 'eip155:84532', // match the session's chain (test: Base Sepolia / live: eip155:8453).
|
|
90
|
+
// only needed for multi-option (allowed_pay_chains) sessions
|
|
91
|
+
token: 'USDC',
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
console.log(data.depositAddress); // buyer sends the exact amount here
|
|
95
|
+
console.log(data.expectedAmount); // atomic μUSDC — display this exact amount
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
> `refundAddress` is committed into the deposit address and **cannot change** after issuance
|
|
99
|
+
> (over / under / late payments auto-refund there on-chain).
|
|
100
|
+
|
|
101
|
+
### Poll status (buyer UX)
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
const { data } = await bitpal.checkout.getStatus(session.id); // no auth needed
|
|
105
|
+
// status: awaiting_deposit → deposit_detected → paid_confirmed → swept
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Polling is for buyer-facing UX. For **server-side** confirmation, trust the webhook (below).
|
|
109
|
+
|
|
110
|
+
### Preview the fee
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
const { data: fee } = await bitpal.checkout.previewFee(toAtomicUSDC('29.00'));
|
|
114
|
+
console.log(fromAtomicUSDC(fee.fee_amount)); // e.g. "0.43"
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Webhooks
|
|
118
|
+
|
|
119
|
+
Register an endpoint in **Developers → Webhooks**. Verify every delivery with the shared secret
|
|
120
|
+
(`whsec_...`) before trusting it.
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
import express from 'express';
|
|
124
|
+
import { verifyWebhookSignature, parseWebhookEvent, WEBHOOK_HEADERS } from '@bitpal/checkout';
|
|
125
|
+
|
|
126
|
+
const app = express();
|
|
127
|
+
|
|
128
|
+
app.post(
|
|
129
|
+
'/webhook',
|
|
130
|
+
express.raw({ type: 'application/json' }), // raw body — do NOT use express.json() (the raw body is signed)
|
|
131
|
+
async (req, res) => {
|
|
132
|
+
const raw = req.body.toString('utf8'); // raw body — parsing/re-serializing breaks the signature
|
|
133
|
+
const ok = await verifyWebhookSignature(
|
|
134
|
+
raw,
|
|
135
|
+
req.header(WEBHOOK_HEADERS.signature) ?? '', // 'sha256=<hex>'
|
|
136
|
+
process.env.BITPAL_WEBHOOK_SECRET!, // whsec_...
|
|
137
|
+
{ timestamp: req.header(WEBHOOK_HEADERS.timestamp) ?? '' }, // replay defense — REQUIRED
|
|
138
|
+
);
|
|
139
|
+
if (!ok) return res.status(400).send('invalid signature');
|
|
140
|
+
|
|
141
|
+
const event = parseWebhookEvent(raw);
|
|
142
|
+
|
|
143
|
+
// ⚠️ At-least-once delivery — dedup on the SIGNED body id (event.id), not the
|
|
144
|
+
// X-Webhook-Id header (unsigned → forgeable). Persist the key in the same txn.
|
|
145
|
+
if (await alreadyProcessed(event.id)) return res.status(200).send('already processed');
|
|
146
|
+
|
|
147
|
+
switch (event.event) {
|
|
148
|
+
case 'checkout.session.paid': // on-chain confirmed
|
|
149
|
+
case 'checkout.session.settled': // swept to your wallet
|
|
150
|
+
await fulfill(event.data.session_id);
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
res.status(200).send('ok');
|
|
154
|
+
},
|
|
155
|
+
);
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Events
|
|
159
|
+
|
|
160
|
+
| Event | Meaning |
|
|
161
|
+
|-------|---------|
|
|
162
|
+
| `checkout.session.created` | Session created |
|
|
163
|
+
| `checkout.session.deposit_detected` | Deposit seen on-chain (awaiting confirmations) |
|
|
164
|
+
| `checkout.session.paid` | Payment confirmed on-chain |
|
|
165
|
+
| `checkout.session.settled` | Funds swept to your wallet |
|
|
166
|
+
| `checkout.session.expired` | Session expired unpaid |
|
|
167
|
+
| `checkout.session.unresolved` | Underpaid / overpaid / wrong token — needs review |
|
|
168
|
+
| `checkout.refund.recorded` | An on-chain auto-refund (over / under / late payment) was recorded |
|
|
169
|
+
|
|
170
|
+
### ⚠️ Replay defense + at-least-once delivery
|
|
171
|
+
|
|
172
|
+
The signature covers `` `${X-Webhook-Timestamp}.${rawBody}` `` (HMAC-SHA256), and `verifyWebhookSignature`
|
|
173
|
+
**requires** the `X-Webhook-Timestamp` header and rejects deliveries outside a ±5 min freshness window
|
|
174
|
+
(override with `toleranceSec`). This blocks replay of a captured request.
|
|
175
|
+
|
|
176
|
+
BitPal may still deliver the **same event more than once** within the window. Deduplicate on the
|
|
177
|
+
**signed body id** (`event.id` from `parseWebhookEvent`), **not** the `X-Webhook-Id` header (the header
|
|
178
|
+
is not signed, so it is forgeable). Persist the dedup key in the **same transaction** as your side
|
|
179
|
+
effect; handlers must be safe to run twice (UPSERT, not INSERT).
|
|
180
|
+
|
|
181
|
+
## Config options
|
|
182
|
+
|
|
183
|
+
```ts
|
|
184
|
+
new BitPal(apiKey); // string shorthand — defaults to https://api.bitpal.io
|
|
185
|
+
new BitPal({ apiKey, baseUrl: 'https://…' }); // config object — override baseUrl (self-hosted / staging)
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## Integration flow
|
|
189
|
+
|
|
190
|
+
1. Backend → `bitpal.checkout.createSession(...)` → get `checkout_url` (+ `session_token`).
|
|
191
|
+
2. Redirect the buyer to `checkout_url` (or render your own UI + `issueDepositAddress`).
|
|
192
|
+
3. Buyer sends the exact amount to the deposit address from any wallet / exchange.
|
|
193
|
+
4. BitPal detects + confirms on-chain, then sweeps directly to your wallet (non-custodial).
|
|
194
|
+
5. Your webhook receives `checkout.session.paid` / `settled` → fulfill the order.
|
|
195
|
+
|
|
196
|
+
## Runnable examples
|
|
197
|
+
|
|
198
|
+
The [source repo](https://github.com/superlabs-x/bitpal-checkout-sdk)'s `examples/` directory
|
|
199
|
+
has runnable references (not shipped in the npm package): `create-session.mjs`,
|
|
200
|
+
`webhook-receiver.mjs`, and a `full-integration/` Express store wiring session creation +
|
|
201
|
+
webhook verification + idempotency.
|
|
202
|
+
|
|
203
|
+
## Build
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
pnpm build # tsup → dist/ (ESM + .d.ts)
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## Migrating from 0.3.x → 0.5.0 (breaking)
|
|
210
|
+
|
|
211
|
+
- **Webhook verification now requires the timestamp** and enforces replay defense. Pass the
|
|
212
|
+
`X-Webhook-Timestamp` header as the 4th argument:
|
|
213
|
+
`verifyWebhookSignature(rawBody, sigHeader, secret, { timestamp: tsHeader })`. A 3-argument call now
|
|
214
|
+
returns `false`. The signed content changed from `body` to `` `${timestamp}.${body}` ``.
|
|
215
|
+
- Deduplicate on the signed `event.id`, not the `X-Webhook-Id` header.
|
|
216
|
+
- Removed legacy pre-release methods (`payWithAuthorization`, `submitExternalPayment`) and escrow refund helpers —
|
|
217
|
+
checkout is non-custodial deposit-address (see Usage above).
|
|
218
|
+
|
|
219
|
+
## License
|
|
220
|
+
|
|
221
|
+
MIT © BitPal. See [LICENSE](./LICENSE).
|