Skip to main content

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.

One-line summary

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:

No refunds — duplicate payment prevention is the top priority

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.

  1. Call POST /v1/payments with the same Idempotency-Key for the same order. The server will dedupe the request and return the same result. (See step 3 for details.)
  2. After a terminal state (PAID/FAILED/EXPIRED/CANCELLED), do not reuse the same checkoutId. Start a new order with a new POST /v1/payments (which uses a new Idempotency-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

Required before going live
  • Reuse the same Idempotency-Key for the same order, and never reuse it after a terminal state.
  • A payment session can be cancelled only while PENDING — once it is PROCESSING it 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.

ItemWhere it is usedWhere to get it
MERCHANT_API_KEYUsed by the merchant backend as Authorization: Bearer ... when calling POST /v1/paymentsRequest via support@crosspay.ai
Frontend originUsed by the SDK runtime to verify the merchant origin and the checkout runtime originRegister at issuance (deployed dev/staging/production origins). localhost is not currently supported — use a reachable HTTPS origin.
Webhook endpointReceives payment confirmation/failure/expiry eventsPrepare an HTTPS endpoint and request registration
Webhook secretHMAC-SHA256 key for signature verificationDelivered out-of-band once at issuance (keep in a secret manager)
CSP settingsAllow CDN scripts and widget iframesConfigure on the merchant frontend
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;
Never send the secret key to the browser

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.

ItemJavaScript SDKReact SDK
Package@nexus-cross/cross-pay-sdk@nexus-cross/cross-pay-react
Recommended forStatic HTML, Vanilla JS, server-rendered pages, other frameworks (Vue, Svelte, etc.)React 18/19 SPA, Next.js, Remix
API shapeloadCrossPay() + crossPay.checkout({...})<CrossPayProvider> + usePayment() hook
State managementHandle await / Promise yourselfThe hook exposes isLoading/error automatically
SSRReturns 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 use the same payment flow

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"
}
Cautions
  • Never expose server-only values (MERCHANT_API_KEY, the Idempotency-Key, the full PG response body, payment_id, etc.) to the client. The client only needs checkout_id.
  • The Idempotency-Key header is a UUID generated by the merchant (use the same value when retrying the same order).
  • payer_uid must 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.
  • country is an optional UI hint. It is only appended to checkout_url as ?country=XX in 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>
);
}
Choosing a checkout mode
  • 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

SituationCauseHow to handle
USER_REJECTEDThe user closed the checkout windowDo not treat as an error; just restore the UI
PAYMENT_FAILED / PAYMENT_DECLINEDThe PG declined the paymentShow a failure message and prompt for retry
ScriptLoadFailedErrorCDN blocked / network failureCheck script-src in your CSP
ScriptLoadTimeoutErrorFailed to load within 15 secondsInspect 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');
}
);
Preserving the raw body is required

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

TopicWhere
Event catalog, payload structure, state machineWebhooks Overview
Signature algorithm, replay protection, code samplesVerifying Webhooks
Retry policy, idempotency, dedupDelivery & Retry
Production checklistWebhooks production checklist
Exact field dictionary per eventAPI: Webhook Events reference

6. Pre-production checklist

Review the following before moving your integration to production.

Server

  • MERCHANT_API_KEY and the webhook secret are stored only in environment variables / a secret manager — never committed to code
  • When calling POST /v1/payments, the Idempotency-Key header 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-id dedup 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_KEY is not exposed in bundles, source maps, or network responses
  • After a terminal state (PAID/FAILED/EXPIRED/CANCELLED), do not reuse the same checkoutId
  • CSP includes script-src https://js.crosspay.ai and frame-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