On this page
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:
<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:
<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:
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;
}
});
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:
- —
allow-scriptsis required for checkout interactivity, API calls, timers, copy buttons, and live status updates. - —
allow-same-originlets the Paymos-hosted checkout run with its own origin instead of an opaque sandbox origin. - —
allow-formsallows normal form interactions inside checkout. - —
allow-top-navigationis omitted so the iframe cannot navigate the merchant page. - —
allow-popupsis omitted because the checkout does not need to open extra browser windows.
When to use Low-Code SDK instead
Use Low-Code SDK 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:* CustomEvents 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.