Skip to content

Use the xMoney inline checkout SDK when you want customers to pay directly on your website without redirecting to the hosted checkout page. The SDK renders secure iframes for card, Apple Pay, Google Pay, and saved card flows, while your server keeps control of order creation and final reconciliation.

This guide focuses on the full XMoney.paymentForm() flow. For custom layouts with separate card and wallet components, see Custom layouts.

How it works

XMoneyAPISecureIframeXMoneySDKMerchantBackendMerchantPageXMoneyAPISecureIframeXMoneySDKMerchantBackendMerchantPagealt[Requires3DS]Create orderorderPayload and orderChecksumXMoney.paymentForm(config)init via postMessageCreate inline checkout sessiononReadySubmit card, wallet, or saved card paymentonPaymentProcessingOpen 3DS modalonPaymentComplete or onError
XMoneyAPISecureIframeXMoneySDKMerchantBackendMerchantPageXMoneyAPISecureIframeXMoneySDKMerchantBackendMerchantPagealt[Requires3DS]Create orderorderPayload and orderChecksumXMoney.paymentForm(config)init via postMessageCreate inline checkout sessiononReadySubmit card, wallet, or saved card paymentonPaymentProcessingOpen 3DS modalonPaymentComplete or onError

Prerequisites

Before rendering the SDK, make sure you have:

  1. A public site key, such as pk_test_... or pk_live_....
  2. A private key stored only on your backend.
  3. A backend endpoint that creates an xMoney order payload and checksum.
  4. A page served over HTTPS in production.
  5. A webhook or server-side status check for final transaction confirmation.
Keep private keys server-side

Never expose your private key in JavaScript, mobile apps, logs, or client-readable configuration. The browser should receive only publicKey, orderPayload, and orderChecksum.

1. Load the SDK

Add the v2 SDK script on the checkout page.

<!-- Production -->
<script src="https://secure.xmoney.com/sdk/v2/xmoney.js"></script>

<!-- Staging -->
<script src="https://secure-stage.xmoney.com/sdk/v2/xmoney.js"></script>

The script exposes window.XMoney.

2. Add a container

Create an empty element where the secure payment form should appear.

<section class="checkout">
  <h2>Payment</h2>
  <div id="xmoney-payment-form"></div>
  <p id="checkout-status" role="status"></p>
</section>

The SDK controls the iframe height automatically.

3. Create the order on your backend

Your backend should build the order request, base64 encode it, sign it with your private key, and return only the values needed by the browser.

import crypto from 'crypto'

function encodePayload(orderData: unknown) {
  return Buffer.from(JSON.stringify(orderData)).toString('base64')
}

function signPayload(orderData: unknown, privateKey: string) {
  return crypto
    .createHmac('sha512', privateKey)
    .update(JSON.stringify(orderData))
    .digest('base64')
}

app.post('/api/checkout/intent', async (req, res) => {
  const orderData = {
    publicKey: process.env.XMONEY_PUBLIC_KEY,
    customer: {
      identifier: req.user.id,
      firstName: req.user.firstName,
      lastName: req.user.lastName,
      country: 'RO',
      city: 'Bucharest',
      email: req.user.email,
    },
    order: {
      orderId: `order-${Date.now()}`,
      description: 'Online order',
      type: 'purchase',
      amount: req.body.amount,
      currency: req.body.currency,
    },
    cardTransactionMode: 'authAndCapture',
    backUrl: 'https://example.com/checkout/result',
    saveCard: Boolean(req.body.saveCard),
  }

  res.json({
    publicKey: process.env.XMONEY_PUBLIC_KEY,
    orderPayload: encodePayload(orderData),
    orderChecksum: signPayload(orderData, process.env.XMONEY_PRIVATE_KEY),
  })
})

The publicKey inside the encoded payload must match the publicKey passed to the SDK.

4. Initialize paymentForm

Fetch the intent from your backend, then mount the payment form.

let paymentForm

async function mountCheckout() {
  const response = await fetch('/api/checkout/intent', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      amount: 4999,
      currency: 'EUR',
      saveCard: true,
    }),
  })

  const { publicKey, orderPayload, orderChecksum } = await response.json()

  paymentForm = await window.XMoney.paymentForm({
    container: 'xmoney-payment-form',
    publicKey,
    orderPayload,
    orderChecksum,
    card: {
      savedCards: {
        enabled: true,
        optInVisible: true,
      },
      submitButton: {
        visible: true,
        type: 'pay',
      },
    },
    paymentMethods: {
      applePay: { enabled: true },
      googlePay: { enabled: true },
    },
    options: {
      locale: 'en-US',
      enableBackgroundRefresh: true,
      appearance: {
        theme: 'light',
      },
    },
    onReady() {
      setCheckoutStatus('Payment form ready')
    },
    onPaymentProcessing(isProcessing) {
      setCheckoutStatus(isProcessing ? 'Processing payment...' : 'Awaiting payment')
    },
    onPaymentComplete(transaction) {
      handlePaymentResult(transaction)
    },
    onError(error) {
      setCheckoutStatus(error?.message || 'Payment form could not be loaded')
    },
  })
}

function setCheckoutStatus(message) {
  document.getElementById('checkout-status').textContent = message
}

function handlePaymentResult(transaction) {
  if (transaction.transactionStatus === 'complete-ok') {
    window.location.href = `/checkout/success?orderId=${transaction.externalOrderId}`
    return
  }

  setCheckoutStatus('Payment was not completed. Please try again.')
}

5. Update order details

If the customer changes shipping, discounts, cart contents, or currency, create a fresh payload/checksum on your backend and call updateOrder().

async function refreshCheckoutOrder(cart) {
  const response = await fetch('/api/checkout/intent', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(cart),
  })

  const { orderPayload, orderChecksum } = await response.json()

  const result = await paymentForm.updateOrder({
    orderPayload,
    orderChecksum,
  })

  if (!result.success) {
    setCheckoutStatus(result.error || 'Could not update the order')
  }
}

Do not mutate the payload in the browser. Any amount or order change should be signed again by your backend.

6. Clean up on unmount

The SDK registers iframe and message listeners. Destroy the instance when the checkout page or component is removed.

function unmountCheckout() {
  paymentForm?.destroy()
  paymentForm = null
}

In single-page applications, call destroy() from the component cleanup function before navigating away from checkout.

Result handling

onPaymentComplete(transaction) is a UI callback. Use it to show a success or failure state to the shopper, but do not use it as the only proof that the order should be fulfilled.

Always reconcile the final status server-side using your webhook or a trusted backend API call. A common pattern is:

  1. onPaymentComplete redirects the shopper to your result page.
  2. The result page asks your backend for the order status.
  3. Your backend confirms the transaction status from xMoney or from the webhook data.
  4. Goods or services are released only after server-side confirmation.