# Website Integration

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](/guides/checkout/inline-checkout/custom-layouts).

## How it works

```mermaid
sequenceDiagram
  participant merchantPage as MerchantPage
  participant merchantBackend as MerchantBackend
  participant xMoneySdk as XMoneySDK
  participant secureIframe as SecureIframe
  participant xMoneyApi as XMoneyAPI

  merchantPage->>merchantBackend: Create order
  merchantBackend-->>merchantPage: orderPayload and orderChecksum
  merchantPage->>xMoneySdk: XMoney.paymentForm(config)
  xMoneySdk->>secureIframe: init via postMessage
  secureIframe->>xMoneyApi: Create inline checkout session
  secureIframe-->>merchantPage: onReady
  secureIframe->>xMoneyApi: Submit card, wallet, or saved card payment
  secureIframe-->>merchantPage: onPaymentProcessing
  alt Requires3DS
    xMoneySdk->>secureIframe: Open 3DS modal
  end
  secureIframe-->>merchantPage: onPaymentComplete 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.

```html
<!-- 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.

```html
<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.

```typescript
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.

```javascript
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()`.

```javascript
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.

```javascript
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.


## Related guides

- [Embedded Checkout overview](/guides/checkout/embedded-checkout)
- [Custom layouts](/guides/checkout/inline-checkout/custom-layouts)
- [Saved cards](/guides/checkout/inline-checkout/saved-cards)
- [3DS and payment results](/guides/checkout/inline-checkout/3ds-results)