Skip to Content
入門快速入門

快速入門

目標:端到端完成一筆真實的測試模式付款。完成後你會擁有一個從伺服器 建立的 session、在買家瀏覽器中開啟的結帳,以及投遞到你 localhost 的 已簽章 webhook。

你需要一組測試模式的 API 金鑰對(pk_test_… + sk_test_…) 以及一個 webhook 簽章密鑰(whsec_…)。請在商家儀表板 Developers → API keysDevelopers → Webhooks 中產生。

1. 安裝瀏覽器 SDK

npm install @lartech/infraio-checkout-js

2. 建立結帳 session(伺服器)

Session 是從你的伺服器建立,並使用你的 secret key 做 HMAC-SHA256 簽章。永遠不要把 sk_ 放到瀏覽器。

建立 canonical 簽章字串

METHOD + "\n" + PATH + "\n" + TIMESTAMP + "\n" + BODY

對於一個 unix-timestamp 1715990400POST /b2b/v1/checkout-sessions/quick 請求,body 為 {...},簽章輸入是:

POST /b2b/v1/checkout-sessions/quick 1715990400 {"items":[...],"currency":"USD","success_url":"..."}

HMAC-SHA256(secret_key, signingString) 簽章,輸出 hex。

送出請求

server/create-session.ts
import { fetch } from "undici"; import { createHmac } from "node:crypto"; const PUBLIC_KEY = process.env.INFRAIO_PUBLIC_KEY!; // pk_test_… const SECRET_KEY = process.env.INFRAIO_SECRET_KEY!; // sk_test_… const BASE_URL = "https://api-dev.infraio.xyz"; const path = "/b2b/v1/checkout-sessions/quick"; const body = JSON.stringify({ items: [ { sku: "tee-large-blue", label: "Indigo Tee — L", unit_price: "49.00", quantity: 1, currency: "USD", image_url: "https://your-shop.test/img/tee.png", }, ], currency: "USD", customer_email: "[email protected]", success_url: "https://your-shop.test/return?status=success", cancel_url: "https://your-shop.test/return?status=cancel", external_ref: "ord_1234", // 你的訂單 id;儲存在訂單上,不在 webhook 中回顯 — 用 order_id 查詢 expires_in: 1800, // 秒;預設 30 分鐘 idempotency_key: crypto.randomUUID(), // 安全重試 }); const timestamp = Math.floor(Date.now() / 1000).toString(); const signingInput = ["POST", path, timestamp, body].join("\n"); const signature = createHmac("sha256", SECRET_KEY) .update(signingInput) .digest("hex"); const res = await fetch(`${BASE_URL}${path}`, { method: "POST", headers: { "Content-Type": "application/json", "X-Client-ID": PUBLIC_KEY, "X-Timestamp": timestamp, "X-Signature": signature, }, body, }); // 每個 API 回應都包在一個信封中 — 實際的 payload 在 `data` 之下。 // 頂層的 `code`/`message` 是結果標記(成功時為 `201` / `"created"`)。 const { code, message, data } = await res.json() as { code: number; message: string; data: { session_key: string; checkout_url: string; order_id: string; expires_at: string }; }; const { session_key, checkout_url, order_id, expires_at } = data; // 把 session_key + checkout_url 傳到瀏覽器。order_id 和 expires_at // 用於你自己的帳務記錄。

時鐘偏移很重要。Gateway 會拒絕 X-Timestamp 與伺服器時間相差超過 5 分鐘的請求。請以 NTP 同步你的伺服器;不要信任長時間執行 cron 的時鐘。

3. 開啟結帳(瀏覽器)

checkout.ts
import { loadInfraIo } from "@lartech/infraio-checkout-js"; const sdk = await loadInfraIo(process.env.NEXT_PUBLIC_INFRAIO_PK!); sdk.checkout({ sessionId: session_key, checkoutUrl: checkout_url, mode: "popup", // 預設 onReady: () => console.log("checkout interactive"), onSuccess: ({ sessionId }) => { // 僅 UX 用 — 詳見下方警告。Webhook 才是權威。 location.assign("/thanks"); }, onCancel: () => console.log("buyer closed the popup"), onError: (err) => console.error(err.code, err.message), });

4. 處理 webhook(伺服器)

Webhook 是唯一權威的金流變動訊號。在某些測試網時序下,瀏覽器 回呼(onSuccess)可能在沒有實際付款的情況下觸發 — 永遠不要以 瀏覽器端事件為履約依據。

server/webhook.ts (Next.js App Router)
import { createHmac, timingSafeEqual } from "node:crypto"; const WEBHOOK_SECRET = process.env.INFRAIO_WEBHOOK_SECRET!; // whsec_… export async function POST(req: Request) { const raw = await req.text(); // 原始位元組,不要解析 const signature = req.headers.get("x-signature") ?? ""; const timestamp = req.headers.get("x-timestamp") ?? ""; // 簽章是針對 `${timestamp}.${raw}` 計算 — 不是只簽 body。 const expected = "sha256=" + createHmac("sha256", WEBHOOK_SECRET) .update(`${timestamp}.${raw}`) .digest("hex"); const a = Buffer.from(signature); const b = Buffer.from(expected); if (a.length !== b.length || !timingSafeEqual(a, b)) { return new Response("bad signature", { status: 400 }); } // HTTP body 直接是每事件的 data 物件(沒有 Stripe 風格的外層信封)。 // 事件類型 + 投遞 ID + 時間戳記在 header 中。 const eventType = req.headers.get("x-event") ?? ""; // 例如 "payment.settled" const deliveryId = req.headers.get("idempotency-key") // 對應 X-Delivery ?? req.headers.get("x-delivery") ?? ""; const payload = JSON.parse(raw); switch (eventType) { case "payment.settled": { // payload 帶 order_id、payment_intent_id、tx_hash、 // amount_received、confirmations、結算 token + network 等。 // 它**不**包含訂單上的 `external_ref` — 若你需要對應回去, // 請以 order_id 在伺服端查詢。 const { order_id, tx_hash, amount_received, confirmations } = payload; // 冪等地標記訂單為已付款。請用 deliveryId(X-Delivery)當作 // 去重鍵 — 它在同一筆投遞的所有重試間保持穩定。 // 我們對非 2xx 最多重試 6 次(1m / 5m / 15m / 1h / 6h)。 break; } case "checkout.expired": { /* session TTL 過期 — 釋放庫存 */ break; } case "payment.refund.requested": { /* 顯示在管理員佇列 */ break; } default: // 向前相容:我們可能新增事件。接受並 no-op。 } return new Response("ok", { status: 200 }); }

邊緣情況(body 正規化、密鑰輪替、防重放)請見Webhooks → 簽章驗證

5. 觸發一筆測試付款

測試模式的 session 結算發生於真實的測試網(Sepolia、Base Sepolia、BSC Testnet 等)— 沒有模擬鏈。從對應的 faucet 取得測試 資金,然後轉到結帳頁顯示的存款位址。Webhook 會在達到確認數後 觸發(多數測試網為 1 個確認)。

下一步