Skip to Content
入门快速入门

快速入门

目标:端到端跑通一笔真实的测试模式支付。完成后,你将拥有一个从你的 服务器创建的会话、在买家浏览器中打开的结账,以及一条经过签名投递到 你 localhost 的 Webhook。

你需要一对测试模式 API 密钥(pk_test_… + sk_test_…)以及一个 Webhook 签名密钥(whsec_…)。在 商户仪表板 Developers → API keysDevelopers → Webhooks 下生成它们。

1. 安装浏览器 SDK

npm install @lartech/infraio-checkout-js

2. 创建结账会话(服务端)

会话从你的服务器创建,使用你的 secret key 进行 HMAC-SHA256 签名。 绝不要把 sk_ 放进浏览器。

构造规范化签名字符串

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

对一个 unix 时间戳为 1715990400、body 为 {...}POST /b2b/v1/checkout-sessions/quick 请求,签名输入是:

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 响应都包在一个信封里 — 真正的负载在 `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 + 时间戳在标头里。 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": { // 负载携带 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": { /* 会话 TTL 过期 — 释放库存 */ break; } case "payment.refund.requested": { /* 在审批队列里呈现 */ break; } default: // 向前兼容:我们可能新增事件。接受并 no-op。 } return new Response("ok", { status: 200 }); }

边界情况(body 规范化、密钥轮换、防重放)见 Webhooks → 签名验证

5. 触发一笔测试支付

测试模式会话结算在真实的测试网上(Sepolia、Base Sepolia、BSC Testnet 等)— 没有 mock 链。从对应水龙头领取测试资金,然后向结账页 上显示的充值地址发送。一旦确认数清零(大多数测试网 1 个确认),Webhook 就会触发。

下一步