<!-- Source: https://docs.infraio.xyz/zh-CN/get-started/quickstart -->
<!-- Last updated: 2026-10-04 -->

# 快速入门

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

> **Note:**
>
> 你需要一对**测试**模式 API 密钥(`pk_test_…` + `sk_test_…`)以及一个
> **Webhook 签名密钥**(`whsec_…`)。在 [商户仪表板](https://app.infraio.xyz)
> 的 **Developers → API keys** 与 **Developers → Webhooks** 下生成它们。

## 1. 安装浏览器 SDK

**npm**

```bash
npm install @lartech/infraio-checkout-js
```

**pnpm**

```bash
pnpm add @lartech/infraio-checkout-js
```

**yarn**

```bash
yarn add @lartech/infraio-checkout-js
```

**bun**

```bash
bun add @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。

### 发送请求

```ts filename="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: "buyer@example.com",
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
// 留给你自己记账用。
```

> **Warning:**
>
> 时钟漂移很重要。API 会拒绝 `X-Timestamp` 与服务器时间相差超过
> **5 分钟**的请求。请用 NTP 同步服务器;不要信任长跑 cron 的墙上时钟。

## 3. 打开结账(浏览器)

**Popup**

```ts filename="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),
});
```

**Redirect**

```ts filename="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: "redirect",
});
// 浏览器导航到 checkout.infraio.xyz;买家随后从创建会话时的
// `success_url` / `cancel_url` 返回。
```

**Embed**

```ts filename="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: "embed",
  container: "#checkout-container",   // 你的 DOM 目标
  onReady: () => /* iframe loaded */ {},
});
```

## 4. 处理 Webhook(服务端)

Webhook 是钱真的动了的**唯一**权威信号。浏览器回调(`onSuccess`)在某些
测试网时序下,即使没有真支付也可能触发 — 永远不要从浏览器侧事件做履约。

```ts filename="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 → 签名验证](https://docs.infraio.xyz/zh-CN/webhooks/signature-verification)。

## 5. 触发一笔测试支付

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

TRON Nile、Solana Devnet 和 TON Testnet(即将推出)没有独立收款地址:买家通过结账页用钱包付款。见[直达钱包的网络](https://docs.infraio.xyz/zh-CN/concepts/chains#直达钱包的网络)。

## 下一步

- [概念 → 会话](https://docs.infraio.xyz/zh-CN/concepts/sessions) — 三层数据模型
  (CheckoutSession + Order + PaymentIntent)。
- [概念 → 链与资产](https://docs.infraio.xyz/zh-CN/concepts/chains) — 哪些网络与 token
  在 live 与 test 上受支持。
- [Webhooks → 概览](https://docs.infraio.xyz/zh-CN/webhooks/overview) — 每种事件类型,以及
  触发条件。
- [安全 → API 密钥](https://docs.infraio.xyz/zh-CN/security/api-keys) — scope、轮换、限流。
