<!-- Source: https://docs.infraio.xyz/ja/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. チェックアウトセッションの作成 (サーバー)

セッションはサーバーから作成し、**シークレット** キーを使った
HMAC-SHA256 で署名します。`sk_` をブラウザに置かないでください。

### canonical 署名文字列を構築

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

unix-timestamp `1715990400`、ボディ `{...}` で
`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",        // your order id; stored on the order, not echoed in webhooks — look it up via order_id
expires_in: 1800,                // seconds; default 30 min
idempotency_key: crypto.randomUUID(), // safe to retry
});

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,
});

// Every API response is wrapped in an envelope — the actual payload
// is under `data`. Treat `code`/`message` at the top level as the
// outcome marker (`201` / `"created"` for success).
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;
// Send session_key + checkout_url to the browser. order_id and expires_at
// are for your own bookkeeping.
```

> **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",            // default
  onReady:   () => console.log("checkout interactive"),
  onSuccess: ({ sessionId }) => {
    // UX only — see the warning below. Webhook is authoritative.
    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",
});
// Browser navigates to checkout.infraio.xyz; the buyer returns to
// your `success_url` / `cancel_url` from the create-session payload.
```

**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",   // your DOM target
  onReady: () => /* iframe loaded */ {},
});
```

## 4. Webhook の処理 (サーバー)

Webhook は資金が動いたことの **唯一の** 正の信号です。ブラウザ
コールバック (`onSuccess`) は一部のテストネットタイミングでは実際の
決済なしに発火することがあります — ブラウザ側のイベントから決して
fulfilment しないでください。

```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();                    // RAW bytes, not parsed
  const signature = req.headers.get("x-signature") ?? "";
  const timestamp = req.headers.get("x-timestamp") ?? "";

  // The signature is over `${timestamp}.${raw}` — NOT the body alone.
  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 });
  }

  // The HTTP body IS the per-event data object (no Stripe-style outer
  // envelope). The event type + delivery ID + timestamp live in headers.
  const eventType = req.headers.get("x-event") ?? "";        // e.g. "payment.settled"
  const deliveryId = req.headers.get("idempotency-key")      // mirrors X-Delivery
                   ?? req.headers.get("x-delivery") ?? "";

  const payload = JSON.parse(raw);
  switch (eventType) {
    case "payment.settled": {
      // The payload carries order_id, payment_intent_id, tx_hash,
      // amount_received, confirmations, settlement token + network, etc.
      // It does NOT include your `external_ref` from the order — look it
      // up server-side via order_id if you need to map back.
      const { order_id, tx_hash, amount_received, confirmations } = payload;

      // Idempotently mark order paid. Use deliveryId (X-Delivery) as the
      // dedup key — it's stable across all retries of the same delivery.
      // We retry up to 6 times (1m / 5m / 15m / 1h / 6h) on non-2xx.
      break;
    }
    case "checkout.expired": { /* session TTL elapsed — free inventory */ break; }
    case "payment.refund.requested": { /* surface in admin queue */ break; }
    default:
      // Forward-compat: we may add events. Accept and no-op.
  }
  return new Response("ok", { status: 200 });
}
```

エッジケース (ボディの正規化、シークレットローテーション、リプレイ保護)
は [Webhook → 署名検証](https://docs.infraio.xyz/ja/webhooks/signature-verification) を参照してください。

## 5. テスト決済を発火

テストモードのセッションは **実際のテストネット** (Sepolia、Base
Sepolia、BSC Testnet など) に対して精算します — モックチェーンは
ありません。該当 faucet からテストファンドを取得し、チェックアウトページが
表示する入金先アドレスに送金してください。confirmations が完了する
(ほとんどのテストネットで 1 conf) と Webhook が発火します。

TRON Nile、Solana Devnet、TON Testnet（近日公開）には入金先アドレスがありません。バイヤーはチェックアウトページからウォレットで支払います。[ウォレット直接決済ネットワーク](https://docs.infraio.xyz/ja/concepts/chains#ウォレット直接決済ネットワーク) を参照してください。

## 次に

- [コンセプト → セッション](https://docs.infraio.xyz/ja/concepts/sessions) — 3 階層のデータ
  モデル (CheckoutSession + Order + PaymentIntent)。
- [コンセプト → チェーン & アセット](https://docs.infraio.xyz/ja/concepts/chains) — どの
  ネットワークとトークンが live / test でサポートされているか。
- [Webhook → 概要](https://docs.infraio.xyz/ja/webhooks/overview) — 全イベントタイプとその
  発火条件。
- [セキュリティ → API キー](https://docs.infraio.xyz/ja/security/api-keys) — スコープ、
  ローテーション、レート制限。
