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

# 빠른 시작

목표: 실제 테스트 모드 결제를 엔드 투 엔드로 처리합니다. 이 가이드를 완료하면
서버에서 생성한 세션, 구매자 브라우저에서 열린 체크아웃, 그리고 로컬호스트로
전달된 서명된 웹훅을 확보하게 됩니다.

> **Note:**
>
> **테스트** API 키 쌍(`pk_test_…` + `sk_test_…`)과 **웹훅 서명 시크릿**(`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_`는 절대 브라우저에 노출하지 마세요.

### 정규화된 서명 문자열 작성

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

유닉스 타임스탬프 `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",        // 가맹점 주문 ID. 주문에 저장되지만 웹훅에는 노출되지 않음 — 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 응답은 envelope로 감싸져 있으며, 실제 페이로드는
// `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는 서버 시간과 **5분** 이상
> 차이 나는 `X-Timestamp`를 가진 요청을 거부합니다. NTP를 통해 서버를 동기화하고,
> 장시간 실행되는 크론의 월 클록은 신뢰하지 마세요.

## 3. 체크아웃 열기(브라우저)

**팝업**

```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 용도만 — 아래 경고 참조. 웹훅이 권위 있는 소스입니다.
    location.assign("/thanks");
  },
  onCancel: () => console.log("buyer closed the popup"),
  onError:  (err) => console.error(err.code, err.message),
});
```

**리디렉션**

```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`로 돌아옵니다.
```

**임베드**

```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. 웹훅 처리(서버)

웹훅은 자금 이동을 알리는 **유일한** 권위 있는 신호입니다. 브라우저 콜백
(`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}`에 대한 것이며, 본문 단독이 아닙니다.
  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 본문이 곧 이벤트별 데이터 객체입니다(Stripe 스타일의 외부
  // envelope 없음). 이벤트 타입 + 전달 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, settlement token + network 등이 포함됩니다.
      // 주문의 `external_ref`는 포함되지 않습니다 — 매핑이 필요하면 order_id로
      // 서버 측에서 조회하세요.
      const { order_id, tx_hash, amount_received, confirmations } = payload;

      // 멱등성을 유지하며 주문을 결제 완료로 표시합니다. deliveryId(X-Delivery)를
      // dedup 키로 사용하세요 — 동일한 전달의 모든 재시도에서 안정적입니다.
      // non-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 });
}
```

엣지 케이스(본문 정규화, 시크릿 로테이션, 리플레이 보호)는 [웹훅 → 서명
검증](https://docs.infraio.xyz/ko/webhooks/signature-verification)을 참조하세요.

## 5. 테스트 결제 트리거

테스트 모드 세션은 **실제 테스트넷**(Sepolia, Base Sepolia, BSC Testnet 등)에서
정산되며 목 체인은 없습니다. 해당 포셋에서 테스트 자금을 받은 후, 체크아웃
페이지에 표시되는 주문별 입금 주소로 전송하세요. 확인이 완료되면(대부분의 테스트넷에서
1 confirmation) 웹훅이 발생합니다.

TRON Nile, Solana Devnet, TON Testnet(출시 예정)에는 주문별 입금 주소가 없습니다. 구매자는 체크아웃 페이지를 통해 지갑에서 결제합니다. [지갑 직접 결제 네트워크](https://docs.infraio.xyz/ko/concepts/chains#지갑-직접-결제-네트워크)를 참조하세요.

## 다음 단계

- [개념 → 세션](https://docs.infraio.xyz/ko/concepts/sessions) — 3단계 데이터 모델(CheckoutSession +
  Order + PaymentIntent)을 다룹니다.
- [개념 → 체인 및 자산](https://docs.infraio.xyz/ko/concepts/chains) — 라이브 및 테스트 환경에서
  지원되는 네트워크와 토큰을 다룹니다.
- [웹훅 → 개요](https://docs.infraio.xyz/ko/webhooks/overview) — 모든 이벤트 타입과 그 트리거를
  다룹니다.
- [보안 → API 키](https://docs.infraio.xyz/ko/security/api-keys) — 스코프, 로테이션, 레이트 리밋을
  다룹니다.
