# Hosted Checkout

> Redirect customers to Hosted Checkout or embed the payment page in a restricted iframe, then fulfil orders from the signed confirmation webhook.

Use the `payment_url` returned by `POST /v1/invoices` as the canonical checkout URL. The same URL supports two presentation modes — full-page redirect and embedded iframe.

Both modes work on any project except a Telegram-bot one. There the same `payment_url` is a deep link into the Paymos bot — no hosted page to redirect to or embed.

## Redirect mode

Redirect mode is the simplest integration. Send the customer to `payment_url` after your server creates the invoice:

```html
<a href="https://checkout.paymos.io/invoice/inv_xxx">Pay invoice</a>
```

The customer completes the payment on the Paymos-hosted page. Your server fulfills the order only after receiving and verifying the signed webhook event.

## Embed mode

Embed mode uses the same `payment_url` with `?embed=true`. Use this when your server already creates invoices and you only want the hosted checkout UI inside your own page:

```html
<iframe
  src="https://checkout.paymos.io/invoice/inv_xxx?embed=true"
  title="Paymos checkout"
  allow="clipboard-write"
  sandbox="allow-scripts allow-same-origin allow-forms"
  style="display:block;width:100%;max-width:460px;height:min(90vh,900px);border:0;"
></iframe>
```

## Embed events

The checkout iframe sends `postMessage` transport events to the parent page:

| Event | Meaning |
|---|---|
| `paymos:succeeded` | Checkout reached `paid` or `paid_over`. |
| `paymos:failed` | Checkout reached a terminal failure state such as `underpaid`, `expired`, or `cancelled`. |
| `paymos:close` | Customer clicked a close/back action inside the embedded checkout. |

Parent pages must validate both the sender origin and the iframe source:

```javascript
const iframe = document.querySelector('#paymos-checkout');

window.addEventListener('message', (event) => {
  if (event.origin !== 'https://checkout.paymos.io') return;
  if (event.source !== iframe.contentWindow) return;
  if (!event.data || typeof event.data !== 'object') return;

  switch (event.data.type) {
    case 'paymos:succeeded':
      console.log('Payment UI succeeded:', event.data.invoice_id);
      break;
    case 'paymos:failed':
      console.warn('Payment UI failed:', event.data.reason);
      break;
    case 'paymos:close':
      iframe.remove();
      break;
  }
});
```

:::danger
Treat these events as UX hints, not proof of payment. Drive spinners, redirects, and analytics from them — but never ship goods, credit balances, or mark an order paid based on a `postMessage`. A `postMessage` is trivial to spoof: any page sharing the customer's origin can post a fake `paymos:succeeded`. Settlement is confirmed only by the signed server-side webhook — the one source of truth for fulfillment.
:::

## Security model

The recommended iframe sandbox is deliberately small:

:::dash-list
- `allow-scripts` is required for checkout interactivity, API calls, timers, copy buttons, and live status updates.
- `allow-same-origin` lets the Paymos-hosted checkout run with its own origin instead of an opaque sandbox origin.
- `allow-forms` allows normal form interactions inside checkout.
- `allow-top-navigation` is omitted so the iframe cannot navigate the merchant page.
- `allow-popups` is omitted because the checkout does not need to open extra browser windows.
:::

## When to use Low-Code SDK instead

Use [Low-Code SDK](/docs/widget-sdk.md) when you want Paymos to create invoices from browser-side config, render the payment button, manage the modal lifecycle, validate iframe messages, and dispatch `paymos:*` `CustomEvent`s for you.

Use manual iframe embed when your backend already owns invoice creation and you only need to place the hosted checkout UI inside your page.
