クイックスタート
ゴール: 実際のテストモード決済をエンドツーエンドで完了させます。終了時には、 サーバーから作成されたセッション、バイヤーのブラウザで開かれたチェックアウト、 そしてあなたの localhost に配信された署名付き Webhook が揃っているはずです。
テスト API キーペア (pk_test_… + sk_test_…) と
Webhook 署名シークレット (whsec_…) が必要です。マーチャント
ダッシュボード の Developers →
API keys と Developers → Webhooks で発行してください。
1. ブラウザ SDK のインストール
npm
npm install @lartech/infraio-checkout-js2. チェックアウトセッションの作成 (サーバー)
セッションはサーバーから作成し、シークレット キーを使った
HMAC-SHA256 で署名します。sk_ をブラウザに置かないでください。
canonical 署名文字列を構築
METHOD + "\n" + PATH + "\n" + TIMESTAMP + "\n" + BODYunix-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 で出力します。
リクエストを送信
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", // 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.時計のずれが重要です。ゲートウェイは X-Timestamp がサーバー時刻と
5 分 以上ずれているリクエストを拒否します。サーバーは NTP で
同期してください; 長時間稼働する cron の壁時計を信用しないでください。
3. チェックアウトを開く (ブラウザ)
Popup
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),
});4. Webhook の処理 (サーバー)
Webhook は資金が動いたことの 唯一の 正の信号です。ブラウザ
コールバック (onSuccess) は一部のテストネットタイミングでは実際の
決済なしに発火することがあります — ブラウザ側のイベントから決して
fulfilment しないでください。
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 → 署名検証 を参照してください。
5. テスト決済を発火
テストモードのセッションは 実際のテストネット (Sepolia、Base Sepolia、BSC Testnet など) に対して精算します — モックチェーンは ありません。該当 faucet からテストファンドを取得し、チェックアウトページが 表示するデポジットアドレスに送金してください。confirmations が完了する (ほとんどのテストネットで 1 conf) と Webhook が発火します。
次に
- コンセプト → セッション — 3 階層のデータ モデル (CheckoutSession + Order + PaymentIntent)。
- コンセプト → チェーン & アセット — どの ネットワークとトークンが live / test でサポートされているか。
- Webhook → 概要 — 全イベントタイプとその 発火条件。
- セキュリティ → API キー — スコープ、 ローテーション、レート制限。