본문으로 건너뛰기

JavaScript SDK

CROSS Pay 결제 연동을 위한 공식 JavaScript SDK입니다. Thin Client Loader 패턴을 사용하며, 결제 로직은 CDN 런타임(cross-pay-core)이 담당합니다. 브라우저 SDK는 API key를 보관하지 않고, 가맹점 백엔드가 만든 checkout session만 엽니다.

호환성

환경지원 버전
Node.js20 LTS 이상 (SSR 로드 시 stub 반환, 실제 결제는 브라우저에서)
브라우저Chrome 120+ · Safari 17+ · Firefox 121+ · Edge 120+
TypeScript5.0+ (타입 정의 번들됨)
Module formatsESM · CJS

설치

pnpm add @nexus-cross/cross-pay-sdk

사전 준비

모든 예시의 checkoutId / idempotencyKey가맹점 백엔드POST /v1/payments를 호출해 발급받은 뒤 클라이언트로 전달한 값입니다.

  • MERCHANT_API_KEY는 서버 환경변수 또는 secret manager에만 저장합니다.
  • 브라우저에는 checkout_ididempotency_key만 전달합니다.
  • optional country는 billing/default UI hint입니다. POST /v1/payments 응답의 checkout_url에만 ?country=XX로 붙고 DB에는 저장되지 않습니다.
  • iframe/popup 결과는 주문 확정 근거가 아닙니다. 최종 처리는 webhook 또는 서버 조회로 확정하세요.

30초 Quick Start

<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>CROSS Pay Quick Start</title>
</head>
<body>
<main>
<h1>CROSS Pay 30초 Quick Start</h1>
<button id="pay" type="button">결제</button>
<pre id="result" aria-live="polite"></pre>
</main>

<script type="module">
import { loadCrossPay, CrossPaySDKError } from 'https://esm.sh/@nexus-cross/cross-pay-sdk';

const button = document.getElementById('pay');
const resultBox = document.getElementById('result');
const checkoutId = 'cs_발급받은_checkout_id';
const idempotencyKey = 'ik_발급받은_idempotency_key';

button.addEventListener('click', async () => {
button.disabled = true;
resultBox.textContent = '결제창 여는 중...';

try {
const crossPay = await loadCrossPay();
const result = await crossPay.checkout({
checkoutId,
idempotencyKey,
appearance: { theme: 'light' },
locale: 'ko',
embeddedWallet: true,
});

resultBox.textContent = JSON.stringify(result, null, 2);
} catch (error) {
if (error instanceof CrossPaySDKError && error.code === 'USER_REJECTED') {
resultBox.textContent = '사용자가 결제창을 닫았습니다.';
} else {
resultBox.textContent = error instanceof Error ? error.message : String(error);
}
} finally {
button.disabled = false;
}
});
</script>
</body>
</html>

초기화 옵션

import { loadCrossPay } from '@nexus-cross/cross-pay-sdk';

const crossPay = await loadCrossPay({
src: 'https://js.crosspay.ai/pay-js/latest/cross-pay.js',
integrity: 'sha384-abc123...',
priority: 'high',
timeout: 15000,
isForceInit: false,
});

integrity가 있으면 SDK는 기존 window.CrossPay 재사용을 건너뛰고 CDN 응답에 대해 SRI 검증이 실제로 수행되도록 합니다. loadCrossPay()는 idempotent하며, 테스트/HMR 환경에서는 clearCrossPay()로 캐시를 비울 수 있습니다.

const result = await crossPay.checkout({
checkoutId,
idempotencyKey,
appearance: { theme: 'auto' },
locale: 'ko',
successUrl: 'https://shop.example.com/pay/success',
failUrl: 'https://shop.example.com/pay/fail',
embeddedWallet: true,
});

console.log(result.paymentId, result.status);

embeddedWallet 기본값은 true입니다. false로 지정하면 crossx connector가 사용 가능한 환경에서도 embedded wallet/social row를 숨깁니다.

사용자 취소는 최신 런타임에서 USER_REJECTED로 reject될 수 있습니다. 결제 실패(PAYMENT_FAILED, PAYMENT_DECLINED)와 사용자 취소를 분리해 처리하세요.

Widget 모드

결제수단 선택 iframe을 페이지에 embed합니다.

const widgets = crossPay.widgets({
appearance: { theme: 'light' },
locale: 'en',
});

widgets.setAmount({ value: 50000, currency: 'KRW' });
widgets.setTheme('dark');
widgets.setLocale('ko');

const paymentMethodWidget = widgets.renderPaymentMethods({
selector: '#payment-methods',
checkoutId: 'cs_abc123',
idempotencyKey: 'ik_xyz',
embeddedWallet: true,
});

paymentMethodWidget.on('ready', () => console.log('widget ready'));
paymentMethodWidget.on('paymentMethodSelect', selected => console.log(selected));

paymentMethodWidget.update({ embeddedWallet: false });

const result = await widgets.submit({ idempotencyKey: 'ik_xyz' });

paymentMethodWidget.destroy();
widgets.destroy();

idempotencyKeyrenderPaymentMethods() 시점에 iframe bridge로 전달됩니다. URL query string에는 절대 포함되지 않으며, 일부 런타임 버전과의 호환성을 위해 widgets.submit()에도 같은 값을 전달할 수 있습니다.

Redirect와 runtime bridge

초기 checkout URL에는 checkoutId, parentOrigin, theme, locale, country, redirect_url, redirect_scheme처럼 routing/UI용 non-secret 값만 넣습니다. idempotencyKey, API key, token, PII는 URL, analytics, error log, screenshot에 남기지 않습니다.

SDK/runtime 사이의 postMessage는 runtime origin과 source window를 검증하고, SDK는 pinned target origin으로만 메시지를 보냅니다.

에러 처리

import {
CrossPaySDKError,
ScriptLoadFailedError,
ScriptLoadTimeoutError,
ScriptLoadAbortedError,
NamespaceNotAvailableError,
} from '@nexus-cross/cross-pay-sdk';

try {
const crossPay = await loadCrossPay();
await crossPay.checkout({ checkoutId: 'cs_abc123', idempotencyKey: 'ik_xyz' });
} catch (err) {
if (err instanceof ScriptLoadFailedError) {
// CDN 스크립트 로드 실패 (네트워크 / 404 / CSP 차단)
} else if (err instanceof ScriptLoadTimeoutError) {
// 스크립트 로드 타임아웃 (기본 15초)
} else if (err instanceof ScriptLoadAbortedError) {
// 호출 전에 명시적으로 abort한 경우
} else if (err instanceof NamespaceNotAvailableError) {
// 스크립트는 로드되었으나 window.CrossPay 미정의
} else if (err instanceof CrossPaySDKError) {
console.error(err.type, err.code, err.message, err.checkoutId, err.retryable);
}
}

멱등성 라이프사이클

상황동작
동일 세션 재시도 (네트워크 실패, popup 재오픈 등)같은 key 재사용 — 서버가 dedupe하여 동일 결과 반환
결제가 terminal 상태(PAID/FAILED/EXPIRED/CANCELLED) 도달이 key 버림 — 다음 결제엔 새 POST /v1/payments 호출로 새 key 발급
다른 주문/amount/customer로 새 결제반드시 새 key — 이전 key 재사용 시 금액 불일치 거부 가능

유틸리티

import { clearCrossPay, validateRedirectUrl, validateScriptUrl } from '@nexus-cross/cross-pay-sdk';

validateRedirectUrl('https://example.com/callback');
validateScriptUrl('https://js.crosspay.ai/pay-js/latest/cross-pay.js');
clearCrossPay(); // 테스트 / HMR 용

SSR

loadCrossPay는 서버 환경(typeof window === 'undefined')에서 호출되면 stub을 resolve합니다. stub의 결제 메서드는 호출 시 Error를 throw하므로 실제 결제 로직은 클라이언트에서만 실행해야 합니다.