Embedding the Payment Page via iframe (for Merchants)

This guide explains how to embed the Payment Page into your store's website with an <iframe> so that everything works correctly — in particular the "Copy" buttons for the bank account number / amount / reference code.

The Payment Page is the URL (payment_url) returned by the FLEX endpoint (/payment-flex/create) — see "Create Payment — FLEX (Recommended)".

Why customers can't use "Copy" — the most common cause: the store page that embeds the iframe is served over HTTP. The browser then treats the whole iframe as a "non-secure context" and disables the Clipboard API entirely. Fix it with steps 1–2 below.


✅ Key checklist (follow all of these and you're done)

  1. [ ] The page that embeds the iframe must be HTTPS (never HTTP)
  2. [ ] Add the allow="clipboard-write" attribute to the <iframe> tag
  3. [ ] Give it enough width/height (width ≥ ~360px) or full-screen on mobile — if the frame is wider than 480px the page will auto-fit (zoom) its content (see §3)
  4. [ ] Do not add sandbox unless you have to (if you really must, see §4)

The correct code (copy-paste ready)

<iframe
  src="{{payment_url}}"
  allow="clipboard-write"
  title="Payment"
  style="width:100%; max-width:480px; height:860px; border:0; border-radius:12px;">
</iframe>

Set src to the payment_url value returned in the create-order API response — use it exactly as received. Do not hardcode the domain or build the URL yourself; treat payment_url as opaque, since its format may change in the future.


1. Why the embedding page must be HTTPS (the most important point)

Even though the Payment Page itself is already HTTPS, the browser's Secure Contexts standard only treats an iframe as "secure" when both the iframe itself and its parent (the page embedding it) are HTTPS at every level.

So if the store page is HTTP → the whole iframe becomes a non-secure context → the browser will:

Fix: serve the store page that hosts the iframe over HTTPS only.

Note: conversely, an HTTPS parent page cannot embed an HTTP iframe (the browser blocks it as mixed content) — but ours is already HTTPS, so that's not an issue here.


2. Why you must add allow="clipboard-write"

Beyond the HTTPS requirement, writing to the clipboard from an iframe served from a different domain (cross-origin) is governed by a second layer — Permissions Policy. By default the browser does not allow a cross-origin iframe to write to the clipboard unless the parent page explicitly "grants" that permission via the allow attribute.

<iframe src="{{payment_url}}" allow="clipboard-write"></iframe>

Our system already includes a fallback on the Payment Page side so copying still works even when the Clipboard API is disabled — but getting steps 1–2 right is the best and most reliable approach.


3. Sizing and rendering (Responsive + auto-fit)

The Payment Page is designed mobile-first (the design references a width of ~480px). Recommended:

Auto-fit content to the frame (auto-fit / zoom)

Previously, if the <iframe> was set wider than ~480px, the content was centered with empty margins on the left and right and the text looked small and hard to read. The Payment Page now detects that it is embedded inside an iframe and automatically zooms its content to fit the frame width — no extra configuration is required on the store side.

<iframe> widthPayment Page behavior
≤ 480pxNormal mobile rendering, full width (no zoom)
480–620pxZooms the content up to fill the frame — larger, easier-to-read text, no empty margins
620–1024pxKeeps the zoom level capped at the 620px size; content centered with small left/right margins
≥ 1024pxAutomatically switches to the 2-column desktop layout

⚠️ About height: when the content is zoomed up (frame width 480–620px), the content also grows taller proportionally. So if you make the frame wider, increase height accordingly (roughly height-at-480px × frame-width ÷ 480), otherwise a scrollbar will appear inside the frame. Example: 480px wide uses ~860px tall → 600px wide should use ~1075px tall.

💡 Easiest setup: keep the width at ≤ 480px (no height calculation needed). But if you have more space you can embed wider too — the content adjusts for readability automatically, and at desktop sizes (≥1024px) you get the full 2-column layout.

<style>
  .pay-frame {
    width: 100%;
    max-width: 480px;
    height: 860px;
    border: 0;
    border-radius: 12px;
    box-shadow: 0 8px 30px rgba(0,0,0,.12);
  }
  @media (max-width: 520px) {
    .pay-frame { max-width: 100%; height: 100vh; border-radius: 0; box-shadow: none; }
  }
</style>

<iframe class="pay-frame"
        src="{{payment_url}}"
        allow="clipboard-write"
        title="Payment"></iframe>

The Payment Page auto-fits the content width to the frame (see the subsection above), but it does not send a height auto-resize signal back to the parent page. So set a height large enough for the content (accounting for the zoom in the table above), or make it full-screen on mobile.


4. If you must use sandbox

Normally adding sandbox is not recommended because it disables several capabilities the Payment Page needs to work fully. But if you have to add it, include all of these tokens:

<iframe
  src="{{payment_url}}"
  allow="clipboard-write"
  sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-downloads allow-top-navigation-by-user-activation"
  title="Payment"></iframe>
tokenRequired because
allow-scriptsThe Payment Page is entirely JavaScript
allow-same-originNeeded for WebSocket (real-time status updates), clipboard, and persistence
allow-formsSlip upload
allow-popupsOpening extra windows / links
allow-downloadsSaving / downloading the QR or slip
allow-top-navigation-by-user-activationLets the "Return to store" button navigate out of the iframe (see §5)

⚠️ If you add sandbox but omit allow-same-origin or allow-scripts, the Payment Page will not work at all.


5. The "Return to store" button (Return flow)

When payment completes / times out, the Payment Page shows a button that returns to the redirect_url the merchant set when creating the order. This button navigates within the iframe (it does not take over the whole page).

The result: if redirect_url points to a normal store page, it will load inside the small iframe, which is not what you want. We recommend one of two approaches:

Approach A (recommended): point redirect_url at a small "break out of the frame" page on your store, e.g.

<!-- e.g. https://shop.example.com/return.html — used as redirect_url -->
<!doctype html>
<meta charset="utf-8">
<script>
  // Navigate the whole page (top window) out of the iframe to the real destination
  var dest = "https://shop.example.com/orders/success?id=12345";
  (window.top || window).location.replace(dest);
</script>

Approach B: accept that the destination page renders inside the iframe and make that page embeddable in an iframe (it must not set X-Frame-Options: DENY or a blocking CSP frame-ancestors).

If you use sandbox, you need allow-top-navigation-by-user-activation for Approach A to work.


6. The most reliable alternative (worth considering)

Embedding the Payment Page in an iframe works well if you configure it as above. But for maximum reliability and to avoid browser-specific issues (e.g. third-party cookie restrictions in Safari, bank 3DS redirects, popups), consider:

Neither approach is subject to the iframe's secure-context / clipboard / cookie limitations.


7. Testing (pre-launch checklist)


Contact

If you still have issues after following this guide, please report to our team along with: the browser/device type, the URL of the store page that embeds the iframe (HTTP or HTTPS), and the <iframe> tag code you used.