Skip to Content
시작하기빠른 시작

빠른 시작

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

테스트 API 키 쌍(pk_test_… + sk_test_…)과 웹훅 서명 시크릿(whsec_…)이 필요합니다. 가맹점 대시보드 Developers → API keysDevelopers → Webhooks에서 생성할 수 있습니다.

1. 브라우저 SDK 설치

npm install @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로 출력합니다.

요청 전송

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: "[email protected]", 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은 // 자체 기록 관리용입니다.

클록 스큐(시간 편차)에 주의해야 합니다. 게이트웨이는 서버 시간과 5분 이상 차이 나는 X-Timestamp를 가진 요청을 거부합니다. NTP를 통해 서버를 동기화하고, 장시간 실행되는 크론의 월 클록은 신뢰하지 마세요.

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

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

4. 웹훅 처리(서버)

웹훅은 자금 이동을 알리는 유일한 권위 있는 신호입니다. 브라우저 콜백 (onSuccess)은 일부 테스트넷 타이밍에서 실제 결제 없이도 발생할 수 있으므로 브라우저 측 이벤트로 이행 처리를 진행해서는 안 됩니다.

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

엣지 케이스(본문 정규화, 시크릿 로테이션, 리플레이 보호)는 웹훅 → 서명 검증을 참조하세요.

5. 테스트 결제 트리거

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

다음 단계