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)
- [ ] The page that embeds the iframe must be HTTPS (never HTTP)
- [ ] Add the
allow="clipboard-write"attribute to the<iframe>tag - [ ] 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)
- [ ] Do not add
sandboxunless 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
srcto thepayment_urlvalue returned in the create-order API response — use it exactly as received. Do not hardcode the domain or build the URL yourself; treatpayment_urlas 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:
- Disable
navigator.clipboard(the Clipboard API) → the "Copy" button stops working ← the symptom customers hit - Disable other features that require a secure context too (camera, geolocation, etc.)
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>
- Only
clipboard-writeis needed (the Payment Page only writes to the clipboard, never reads) - You do not need
clipboard-reador any other permission
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:
- Mobile: make it full-width and full-height for the best experience
- Desktop / narrow box: about 420–480px wide, about 820–880px tall
- Width should not be less than ~360px (otherwise the content becomes too cramped)
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> width | Payment Page behavior |
|---|---|
| ≤ 480px | Normal mobile rendering, full width (no zoom) |
| 480–620px | Zooms the content up to fill the frame — larger, easier-to-read text, no empty margins |
| 620–1024px | Keeps the zoom level capped at the 620px size; content centered with small left/right margins |
| ≥ 1024px | Automatically 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
heightaccordingly (roughlyheight-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
heightlarge 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>
| token | Required because |
|---|---|
allow-scripts | The Payment Page is entirely JavaScript |
allow-same-origin | Needed for WebSocket (real-time status updates), clipboard, and persistence |
allow-forms | Slip upload |
allow-popups | Opening extra windows / links |
allow-downloads | Saving / downloading the QR or slip |
allow-top-navigation-by-user-activation | Lets the "Return to store" button navigate out of the iframe (see §5) |
⚠️ If you add
sandboxbut omitallow-same-originorallow-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 needallow-top-navigation-by-user-activationfor 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:
- Full-page redirect: send the user to the Payment Page as a full page, then
redirect_urlback to the store — the most reliable option - Open a new tab/window: open the Payment Page in a new tab
Neither approach is subject to the iframe's secure-context / clipboard / cookie limitations.
7. Testing (pre-launch checklist)
- [ ] Open the store page with the embedded iframe over real HTTPS (not
localhost) - [ ] Press the "Copy" buttons for amount / account number / reference code → they must show
"Copied" (green) and actually paste the value - [ ] Test on real mobile devices (Safari iOS and Chrome Android) — including in-app browsers
if possible (e.g. opened via LINE/Facebook) - [ ] Test embedding at several widths (e.g. 480px, 600px, full-screen) → content fits the
frame, text isn't abnormally small, and there are no excessive left/right margins (see §3) - [ ] Test the "Return to store" button to confirm it returns to the right place (see §5)
- [ ] Test slip upload (if your integration uses it)
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.