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

# 快速入門

目標:端到端完成一筆真實的測試模式付款。完成後你會擁有一個從伺服器
建立的 session、在買家瀏覽器中開啟的結帳，以及投遞到你 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. 建立結帳 session(伺服器)

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

### 建立 canonical 簽章字串

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

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

```
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 回應都包在一個信封中 — 實際的 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
// 用於你自己的帳務記錄。
```

> **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;買家會以 create-session payload
// 中的 `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 載入完成 */ {},
});
```

## 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 + 時間戳記在 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 → 簽章驗證](https://docs.infraio.xyz/zh-TW/webhooks/signature-verification)。

## 5. 觸發一筆測試付款

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

TRON Nile、Solana Devnet 與 TON Testnet(即將推出)沒有獨立收款位址：買家透過結帳頁用錢包付款。請見[直達錢包的網路](https://docs.infraio.xyz/zh-TW/concepts/chains#直達錢包的網路)。

## 下一步

- [概念 → 工作階段](https://docs.infraio.xyz/zh-TW/concepts/sessions) — 三層資料模型
  (CheckoutSession + Order + PaymentIntent)。
- [概念 → 支援的鏈與資產](https://docs.infraio.xyz/zh-TW/concepts/chains) — live 與 test 各
  支援哪些網路與 token。
- [Webhooks → 總覽](https://docs.infraio.xyz/zh-TW/webhooks/overview) — 每種事件類型及觸發
  條件。
- [安全 → API 金鑰](https://docs.infraio.xyz/zh-TW/security/api-keys) — scope、輪替、速率
  限制。
