Getting Started
This single page walks you through the full integration: prerequisites → SDK selection → server endpoint → checkout window → webhook confirmation → pre-production review. Follow it top to bottom.
Your server creates a payment session with MERCHANT_API_KEY → the browser opens the checkout window via the SDK → the server finalizes the order via webhook.
The browser result is not the source of truth for confirmation.
The full payment flow across the client, the merchant server, and the CROSS Pay API:
CROSS Pay currently does not provide a refund feature. If the same order is charged twice, it cannot be reversed. Make sure you follow these two rules.
- Call
POST /v1/paymentswith the sameIdempotency-Keyfor the same order. The server will dedupe the request and return the same result. (See step 3 for details.) - After a terminal state (
PAID/FAILED/EXPIRED/CANCELLED), do not reuse the samecheckoutId. Start a new order with a newPOST /v1/payments(which uses a newIdempotency-Key).
Judging "the payment probably didn't go through" from the browser popup result alone and re-calling can lead to duplicate charges. Always confirm the state via webhook receipt or a server-side GET /v1/payments/{id} lookup before deciding the next action.
Required before production
Duplicate-payment prevention and checkout-session handling are mandatory before you go to production — do not launch without them. The full implementation is covered on a dedicated page:
→ Payment Reliability — duplicate prevention & session management
- Reuse the same
Idempotency-Keyfor the same order, and never reuse it after a terminal state. - A payment session can be cancelled only while
PENDING— once it isPROCESSINGit can no longer be cancelled. - Confirm a payment via webhook receipt (or
GET /v1/payments/{id}), never from the browser result.
1. Prerequisites
Issue and register the following items before starting the integration. Missing any one of them will block you in a later step.
| Item | Where it is used | Where to get it |
|---|---|---|
MERCHANT_API_KEY | Used by the merchant backend as Authorization: Bearer ... when calling POST /v1/payments | Request via support@crosspay.ai |
| Frontend origin | Used by the SDK runtime to verify the merchant origin and the checkout runtime origin | Register at issuance (deployed dev/staging/production origins). localhost is not currently supported — use a reachable HTTPS origin. |
| Webhook endpoint | Receives payment confirmation/failure/expiry events | Prepare an HTTPS endpoint and request registration |
| Webhook secret | HMAC-SHA256 key for signature verification | Delivered out-of-band once at issuance (keep in a secret manager) |
| CSP settings | Allow CDN scripts and widget iframes | Configure on the merchant frontend |
Recommended CSP
Content-Security-Policy:
script-src 'self' https://js.crosspay.ai;
connect-src 'self' https://api.crosspay.io;
frame-src https://widget.crosspay.ai https://checkout.crosspay.ai;
Store MERCHANT_API_KEY only in server environment variables / a secret manager and never pass it to the browser.
The SDK does not create payments; it only opens the hosted checkout window using the checkout_id produced by your server. The Idempotency-Key stays server-side.
2. Pick an SDK
Open the browser checkout window with one of these two SDKs. Pick the one that fits your environment and framework.
| Item | JavaScript SDK | React SDK |
|---|---|---|
| Package | @nexus-cross/cross-pay-sdk | @nexus-cross/cross-pay-react |
| Recommended for | Static HTML, Vanilla JS, server-rendered pages, other frameworks (Vue, Svelte, etc.) | React 18/19 SPA, Next.js, Remix |
| API shape | loadCrossPay() + crossPay.checkout({...}) | <CrossPayProvider> + usePayment() hook |
| State management | Handle await / Promise yourself | The hook exposes isLoading/error automatically |
| SSR | Returns a stub (only works for real in the browser) | The Provider safely handles client-only |
| Recommended when | • One or two checkout pages • Embedded in marketing pages • Must work without a React dependency | • Already a React app • Payment invoked from several components • Manage theme/locale uniformly via Provider |
Both SDKs internally load the same CDN runtime (cross-pay-core).
Think of the React SDK as a thin wrapper around the JS SDK with hooks/Provider on top.
3. Add the payment session endpoint on your server
The client must never call the CROSS Pay API directly.
The payment session is created on the merchant server, which uses MERCHANT_API_KEY to call POST /v1/payments.
Add a payment session creation endpoint on your server like this.
POST /checkout
This endpoint receives product/order information from the client, creates a CROSS Pay payment session, and returns the only value the client needs — the checkout_id from the API response. The Idempotency-Key your server sent is used purely server-side to dedupe creation and is never returned to the client.
Implementation example (Node.js)
// POST /checkout
app.post('/checkout', async (req, res) => {
const { orderId, amount, payerUid } = req.body;
// Generate once and reuse the same value when retrying the same order.
// The API does NOT return this — your server is the source of truth for it.
const idempotencyKey = crypto.randomUUID();
const response = await fetch('https://api.crosspay.io/v1/payments', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.MERCHANT_API_KEY}`,
'Content-Type': 'application/json',
'Idempotency-Key': idempotencyKey,
},
body: JSON.stringify({
currency: 'KRW',
amount: String(amount),
payer_uid: payerUid,
country: 'KR',
metadata: {
order_id: orderId,
product: { name: 'Game Pass Premium', category: 'DIGITAL_GOODS' },
},
}),
});
if (!response.ok) {
return res.status(502).json({ error: 'failed to create checkout' });
}
// payment = { id, checkout_id, checkout_url, status, currency, amount, expires_at, ... }
// There is no `idempotency_key` field in the response.
const payment = await response.json();
// The client only needs the checkout session id. The Idempotency-Key stays
// server-side — it dedupes POST /v1/payments creation and never reaches the browser.
res.json({ checkoutId: payment.checkout_id });
});
Response shape (merchant server → client)
{
"checkoutId": "cs_abc123"
}
- Never expose server-only values (
MERCHANT_API_KEY, theIdempotency-Key, the full PG response body, payment_id, etc.) to the client. The client only needscheckout_id. - The
Idempotency-Keyheader is a UUID generated by the merchant (use the same value when retrying the same order). payer_uidmust be a unique value per payer. Provide a stable identifier that uniquely maps to one payer on your side (e.g. your internal user ID), and never reuse the same value across different payers.countryis an optional UI hint. It is only appended tocheckout_urlas?country=XXin the response and is not stored in the database.
4. Open the checkout window in the browser
Now the client calls the endpoint from step 3 and passes the response to the SDK to open the checkout window.
JavaScript SDK
import { loadCrossPay, CrossPaySDKError } from '@nexus-cross/cross-pay-sdk';
document.getElementById('pay').addEventListener('click', async () => {
// 1) Create a checkout session via the merchant server
const res = await fetch('/checkout', { method: 'POST', body: JSON.stringify({...}) });
const { checkoutId } = await res.json();
// 2) Open the checkout window with the SDK
try {
const crossPay = await loadCrossPay();
const result = await crossPay.checkout({
checkoutId,
appearance: { theme: 'auto' },
locale: 'ko',
embeddedWallet: true,
});
console.log(result.paymentId, result.status);
} catch (error) {
if (error instanceof CrossPaySDKError && error.code === 'USER_REJECTED') {
// The user closed the checkout window — a normal branch
return;
}
// Surface other errors in the UI
console.error(error);
}
});
React SDK
import { CrossPayProvider, CrossPaySDKError, usePayment } from '@nexus-cross/cross-pay-react';
export default function App() {
return (
<CrossPayProvider theme="auto" locale="ko">
<CheckoutButton />
</CrossPayProvider>
);
}
function CheckoutButton() {
const { requestPayment, isLoading } = usePayment();
async function handlePay() {
const res = await fetch('/checkout', { method: 'POST', body: JSON.stringify({...}) });
const { checkoutId } = await res.json();
try {
const result = await requestPayment({ checkoutId, embeddedWallet: true });
console.log(result.paymentId, result.status);
} catch (error) {
if (error instanceof CrossPaySDKError && error.code === 'USER_REJECTED') return;
throw error;
}
}
return (
<button type="button" onClick={handlePay} disabled={isLoading}>
{isLoading ? 'Loading SDK...' : 'Pay'}
</button>
);
}
- Overlay (popup/widget) — Does not leave the merchant page. Good for desktop checkout and in-page payment-method widgets. → Overlay mode guide
- Redirect — Navigates to the hosted checkout and comes back. Good for mobile browsers, app WebViews, and payment methods that require external PG redirection. → Redirect mode guide
Common exceptions
| Situation | Cause | How to handle |
|---|---|---|
USER_REJECTED | The user closed the checkout window | Do not treat as an error; just restore the UI |
PAYMENT_FAILED / PAYMENT_DECLINED | The PG declined the payment | Show a failure message and prompt for retry |
ScriptLoadFailedError | CDN blocked / network failure | Check script-src in your CSP |
ScriptLoadTimeoutError | Failed to load within 15 seconds | Inspect the network and retry |
For more detailed error branches, see JavaScript SDK reference (Error handling section) and Error Codes.
5. Confirm payment via webhook
Do not finalize an order based on the browser popup/redirect result alone.
Final payment confirmation must come from webhook receipt or a server-side GET /v1/payments/{id} lookup.
Minimal implementation (Express)
import express from 'express';
import crypto from 'crypto';
const app = express();
const SECRET = process.env.CROSSPAY_WEBHOOK_SECRET;
app.post(
'/webhooks/crosspay',
express.raw({ type: 'application/json' }), // preserving the raw body is required
(req, res) => {
const id = req.get('webhook-id');
const ts = req.get('webhook-timestamp');
const sig = req.get('webhook-signature') || '';
const secretBytes = Buffer.from(SECRET, 'base64'); // secret is base64-encoded
const expected = crypto
.createHmac('sha256', secretBytes)
.update(`${id}.${ts}.${req.body.toString('utf8')}`)
.digest();
const ok = sig.split(' ').some(pair => {
const [v, b64] = pair.split(',');
if (v !== 'v1' || !b64) return false;
const got = Buffer.from(b64, 'base64');
return got.length === expected.length && crypto.timingSafeEqual(got, expected);
});
if (!ok) return res.status(401).send('invalid signature');
const event = JSON.parse(req.body.toString('utf8'));
// event.type: payment.completed | payment.failed | payment.expired | payment.cancelled
// event.data.payment: { id, status, amount, currency, metadata, ... }
// ... dedup + finalize order ...
res.status(200).send('ok');
}
);
If you parse JSON and then re-serialize, whitespace and key order may change, breaking the signature.
Preserve the original bytes — use express.raw({ type: 'application/json' }) in Express or io.ReadAll(r.Body) in Go.
Where to go deeper
| Topic | Where |
|---|---|
| Event catalog, payload structure, state machine | Webhooks Overview |
| Signature algorithm, replay protection, code samples | Verifying Webhooks |
| Retry policy, idempotency, dedup | Delivery & Retry |
| Production checklist | Webhooks production checklist |
| Exact field dictionary per event | API: Webhook Events reference |
6. Pre-production checklist
Review the following before moving your integration to production.
Server
-
MERCHANT_API_KEYand the webhook secret are stored only in environment variables / a secret manager — never committed to code - When calling
POST /v1/payments, theIdempotency-Keyheader makes same-order retries safe - The webhook endpoint uses HTTPS
- Domain logic only runs after webhook signature verification passes (return 401 on verification failure)
- A
webhook-iddedup table is in place (return 200 or 409 on duplicate arrival) - Webhook responses return within 10 seconds — delegate heavy work to a queue
Client
- Confirm that
MERCHANT_API_KEYis not exposed in bundles, source maps, or network responses - After a terminal state (
PAID/FAILED/EXPIRED/CANCELLED), do not reuse the samecheckoutId - CSP includes
script-src https://js.crosspay.aiandframe-src https://widget.crosspay.ai
Confirmation logic
- Do not finalize an order based on the popup/redirect result alone
- Finalize via webhook receipt or
GET /v1/payments/{id}lookup - After reaching a terminal state, start a new order with a new
POST /v1/payments
Learn more
- JavaScript SDK reference —
loadCrossPay, options, error classes - React SDK reference — Provider, hooks, widget mode
- API Reference — all endpoints + Try-it-out
- Webhooks Overview — events, payloads, state machine
- Error Codes — SDK/API error taxonomy