JavaScript SDK
The official JavaScript SDK for CROSS Pay integration. It uses a Thin Client Loader pattern — the actual payment logic lives in the CDN runtime (cross-pay-core). The browser SDK holds no API keys; it only opens checkout sessions created by your merchant backend.
Compatibility
| Environment | Supported versions |
|---|---|
| Node.js | 20 LTS and later (SSR load returns a stub; the actual payment runs in the browser) |
| Browsers | Chrome 120+ · Safari 17+ · Firefox 121+ · Edge 120+ |
| TypeScript | 5.0+ (type definitions bundled) |
| Module formats | ESM · CJS |
Installation
pnpm add @nexus-cross/cross-pay-sdk
Prerequisites
In every example, checkoutId / idempotencyKey are values your merchant backend obtains by calling POST /v1/payments and passes to the client.
- Store
MERCHANT_API_KEYonly in server environment variables or a secret manager. - The browser only ever receives
checkout_idandidempotency_key. - The optional
countryis a billing/default-UI hint. It is appended as?country=XXto thecheckout_urlreturned byPOST /v1/paymentsand is never persisted. - The iframe/popup result is not authoritative for order fulfillment. Confirm finalization via webhook or server-side lookup.
30-second quick start
<!doctype html>
<html lang="en">
<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-second Quick Start</h1>
<button id="pay" type="button">Pay</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_issued_checkout_id';
const idempotencyKey = 'ik_issued_idempotency_key';
button.addEventListener('click', async () => {
button.disabled = true;
resultBox.textContent = 'Opening checkout...';
try {
const crossPay = await loadCrossPay();
const result = await crossPay.checkout({
checkoutId,
idempotencyKey,
appearance: { theme: 'light' },
locale: 'en',
embeddedWallet: true,
});
resultBox.textContent = JSON.stringify(result, null, 2);
} catch (error) {
if (error instanceof CrossPaySDKError && error.code === 'USER_REJECTED') {
resultBox.textContent = 'The user closed the checkout window.';
} else {
resultBox.textContent = error instanceof Error ? error.message : String(error);
}
} finally {
button.disabled = false;
}
});
</script>
</body>
</html>
Initialization options
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,
});
When integrity is set, the SDK skips reusing an existing window.CrossPay so that SRI verification actually runs against the CDN response. loadCrossPay() is idempotent; in test/HMR environments you can clear the cache with clearCrossPay().
Popup checkout
const result = await crossPay.checkout({
checkoutId,
idempotencyKey,
appearance: { theme: 'auto' },
locale: 'en',
successUrl: 'https://shop.example.com/pay/success',
failUrl: 'https://shop.example.com/pay/fail',
embeddedWallet: true,
});
console.log(result.paymentId, result.status);
embeddedWallet defaults to true. Set it to false to hide the embedded wallet/social row even when the crossx connector is available.
User cancellation can reject with USER_REJECTED on the latest runtime. Handle payment failures (PAYMENT_FAILED, PAYMENT_DECLINED) separately from user cancellation.
Widget mode
Embed the payment-method-selection iframe inline on your page.
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();
idempotencyKey is passed to the iframe bridge at renderPaymentMethods() time. It is never included in the URL query string. For compatibility with some runtime versions, you may also pass the same value to widgets.submit().
Redirect and runtime bridge
The initial checkout URL contains only non-secret routing/UI values such as checkoutId, parentOrigin, theme, locale, country, redirect_url, and redirect_scheme. idempotencyKey, API keys, tokens, and PII must never appear in URLs, analytics, error logs, or screenshots.
postMessage between SDK and runtime validates the runtime origin and source window, and the SDK only sends messages to a pinned target origin.
Error handling
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 script load failed (network / 404 / CSP blocked)
} else if (err instanceof ScriptLoadTimeoutError) {
// Script load timed out (default 15s)
} else if (err instanceof ScriptLoadAbortedError) {
// Aborted explicitly before the call completed
} else if (err instanceof NamespaceNotAvailableError) {
// Script loaded but window.CrossPay is undefined
} else if (err instanceof CrossPaySDKError) {
console.error(err.type, err.code, err.message, err.checkoutId, err.retryable);
}
}
Idempotency lifecycle
| Scenario | Behavior |
|---|---|
| Retry within the same session (network failure, popup re-open, etc.) | Reuse the same key — the server dedupes and returns the same result |
Payment reaches a terminal state (PAID/FAILED/EXPIRED/CANCELLED) | Discard the key — issue a new key by calling POST /v1/payments again for the next payment |
| New payment with a different order/amount/customer | Always a new key — reusing a previous key may be rejected for amount mismatch |
Utilities
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(); // for tests / HMR
SSR
When called in a server environment (typeof window === 'undefined'), loadCrossPay resolves to a stub. The stub's payment methods throw an Error when invoked, so the actual payment logic must run on the client only.