Skip to content

Hosted checkout and widget

With the hosted checkout the card never touches your server or your page. Your server opens a session, your page mounts the widget with the session id, and the shopper pays inside an iframe served by the gateway.

your server ── POST /api/payment-session (x-api-key) ──▶ gateway → sessionId
your page ── widget.open({ sessionId }) ──▶ iframe {checkout}/c/{sessionId}
the iframe ── card ──▶ gateway ──▶ processor
your page ◀── onPaymentReceived / onError / onStatus

Same body as Create a payment, without the card fields. Idempotency-Key is required: sending the same key again returns the same session.

Terminal window
curl -X POST "$BASE_URL/api/payment-session" \
-H "x-api-key: $API_KEY" \
-H "Idempotency-Key: order_1001" \
-H "Content-Type: application/json" \
-d '{
"merchantIdentifier": "...",
"midName": "...",
"referenceCode": "ORDER-1001",
"amount": "19.90",
"currency": "USD",
"email": "buyer@example.com",
"postbackUrl": "https://merchant.example/hooks/pay"
}'

The answer (201) holds sessionId, checkoutUrl, status (CREATED), createdAt and expiresAt. A session lives 30 minutes and starts one sale. Sending the same Idempotency-Key with a different body is refused with 409, so use a new key per order. The final status also reaches your postbackUrl, as for any payment: treat the postback as the source of truth, not the browser callback.

<div id="checkout"></div>
<script src="https://YOUR-HOST/widget.js"></script>
<script>
const widget = PayBridgeWidget.createWidget({
brand: 'paybridge', // or 'xcharger'
});
widget.open({
sessionId, // from step 1; never put the API key in the page
container: 'checkout',
onPaymentReceived: (p) => {}, // APPROVED
onError: (e) => {}, // DECLINED, ERROR, or an unreadable status
onStatus: (s) => {}, // PENDING and other progress
onRedirect: ({ url }) => {}, // 3-D Secure: open url in a popup window (optional)
});
</script>

Your page’s origin must be allowed by the gateway (CORS_ALLOW_ORIGINS). A second open replaces the first iframe. When the card needs 3-D Secure, the checkout stays on “pending” and onRedirect receives the challenge address. Open it in a popup window (the demo does) with window.open. Browsers block popups that do not follow a click, so keep a link the shopper can click as a fallback. Without onRedirect nothing is shown and the shopper waits. Either way the result arrives through onPaymentReceived or onError.

Callback When Argument
onPaymentReceived The payment is APPROVED { paymentId, amount, currency, status, occurredAt }
onStatus The payment is PENDING or otherwise still moving { status }
onError DECLINED, ERROR or an unreadable status { message, status?, paymentId? }
onRedirect The processor needs 3-D Secure { url } (an http(s) address)

widget.close() removes the iframe. The browser callback is for your page’s feedback only. Confirm the order from the postback or a lookup before you ship anything.

What you see Likely cause
The iframe never appears The sessionId is wrong or expired. Open a new session; each one starts a single payment.
409 opening a session You reused an Idempotency-Key with a different body.
Browser blocks the request Your page’s origin is not on the gateway’s allowed origins.
onError after the shopper paid Do not charge again. Look the payment up by referenceCode first.

The widget demo opens a real sandbox session. Use the test cards, and remember that 3-D Secure cards stay inside the iframe.