Skip to navigation

Embed Fluveo Checkout in an iframe

Show the hosted payment page inside your own checkout and learn the outcome.

Status: test-only. Embedding is available for test-mode Checkout Sessions. Card payments, including 3-D Secure, are exercised in test mode.

You can show the hosted payment page of a Checkout Session inside an <iframe> on your own checkout page instead of redirecting to it. The card fields still live on Fluveo’s page, so card data never reaches your page or your servers.

PCI DSS. An embedded payment page is not the same as a redirect for SAQ A eligibility: with an iframe, the page that hosts it must also be protected against script attacks (PCI SSC FAQ 1588). Confirm your scope with your assessor before you switch from a redirect.

How it works:

  1. Your server creates a Checkout Session and gets its url.
  2. Your page puts that url in an iframe. The customer pays inside it.
  3. When the payment reaches a final status, the page in the iframe sends your page a fluveo.checkout.completed message. It does not navigate your page by itself.
  4. Your page asks your server, and your server confirms the outcome with GET /v1/checkout/sessions/{id} before it fulfils the order.

When the card needs 3-D Secure, the customer leaves your page for the challenge and comes back on your success_url instead (see 3-D Secure).

1. Create the Checkout Session (server)

Create the session exactly as for a redirect. success_url and cancel_url must be pages on your site that can finish the order on their own, because a customer who goes through 3-D Secure returns there as a full page.

curl
curl https://api.devfluveo.com/v1/checkout/sessions \
-H "Authorization: Bearer $FLUVEO_API_KEY" \
-d mode=payment \
-d "success_url=https://shop.example.com/order/complete?session_id={CHECKOUT_SESSION_ID}" \
-d "cancel_url=https://shop.example.com/cart" \
-d "line_items[0][price_data][currency]=brl" \
-d "line_items[0][price_data][unit_amount]=4990" \
-d "line_items[0][price_data][product_data][name]=Order 1042" \
-d "line_items[0][quantity]=1"

The answer has the session id (cs_test_...) and its url (https://pay.devfluveo.com/c/cs_test_... in test mode). Send both to your page.

Embed the session url only. A Payment Link URL (/p/...) refuses to be framed.

2. Embed the page (browser)

checkout.html
<iframe
id="fluveo-checkout"
src="https://pay.devfluveo.com/c/cs_test_..."
title="Payment"
allow="payment"
style="width: 100%; min-height: 900px; border: 0"
></iframe>
  • Do not sandbox the iframe. That is the setup Fluveo has tested (Chromium).
  • If your policy requires sandbox, we recommend sandbox="allow-scripts allow-same-origin allow-forms allow-top-navigation-by-user-activation". The 3-D Secure step navigates the whole tab, so it needs allow-top-navigation-by-user-activation. This recipe follows from that mechanism; it has not been tested, so try a 3-D Secure test card in each browser you support.
  • Keep allow="payment" so browser wallets can use the Payment Request API inside the frame.

3. Listen for the completion message (browser)

When the page in the iframe reaches a final status, it posts one message to its parent:

{ "type": "fluveo.checkout.completed", "checkout_session": "cs_test_...", "status": "succeeded" }
FieldMeaning
typeAlways fluveo.checkout.completed.
checkout_sessionThe Checkout Session id.
statusThe payment’s final status: succeeded, requires_capture (manual capture), partially_captured, failed, canceled / cancelled or expired.

The message carries no secret, and it is sent to any parent (target origin *). Anyone who can frame the page can receive it, and any page can post a look-alike message to yours. So:

  • check event.origin: it must be the origin of the session url (https://pay.devfluveo.com in test mode, https://pay.fluveo.com in live mode);
  • check event.source is your iframe and checkout_session is the session you created;
  • never fulfil an order from the message alone. Treat it as “go and check now”.
checkout.html
<script>
const sessionId = "cs_test_..."; // from your server, step 1
const sessionUrl = "https://pay.devfluveo.com/c/cs_test_..."; // the session url
const frame = document.getElementById("fluveo-checkout");
window.addEventListener("message", async (event) => {
if (event.origin !== new URL(sessionUrl).origin) return;
if (event.source !== frame.contentWindow) return;
const data = event.data;
if (!data || data.type !== "fluveo.checkout.completed") return;
if (data.checkout_session !== sessionId) return;
// Ask your server; it reads the session from Fluveo (step 4).
const response = await fetch(`/orders/confirm?session_id=${encodeURIComponent(sessionId)}`);
const order = await response.json();
if (order.paid) {
window.location.assign(`/order/complete?session_id=${encodeURIComponent(sessionId)}`);
}
});
</script>

4. Confirm on your server

Read the session with your secret key and fulfil only when it is paid:

curl
curl https://api.devfluveo.com/v1/checkout/sessions/cs_test_... \
-H "Authorization: Bearer $FLUVEO_API_KEY"
server.js (Node 18+)
// GET /orders/confirm?session_id=cs_test_...
async function confirmOrder(sessionId) {
const response = await fetch(
`https://api.devfluveo.com/v1/checkout/sessions/${encodeURIComponent(sessionId)}`,
{ headers: { Authorization: `Bearer ${process.env.FLUVEO_API_KEY}` } },
);
if (!response.ok) throw new Error(`Fluveo answered ${response.status}`);
const session = await response.json();
const paid = session.status === "complete" && session.payment_status === "paid";
if (paid) {
// Fulfil once: key the fulfilment on session.id so a repeat call does nothing.
}
return { paid };
}

Use the same check on your success_url page. If the customer closes the tab before either runs, a signed webhook payment_intent.succeeded for the session’s PaymentIntent (payment_intent on the session) still reaches your server.

3-D Secure

When the card needs 3-D Secure, the challenge does not stay inside the iframe:

  1. The customer presses Pay inside the iframe.
  2. The whole tab moves to the bank’s challenge page. Your checkout page is no longer shown.
  3. After the challenge, the tab returns to the Fluveo page and then goes to your success_url as a full page.

No fluveo.checkout.completed message is sent in this case, because the Fluveo page is no longer in your iframe. Your success_url page confirms the order with step 4.

Inside an iframe, the paid page does not send the customer anywhere by itself: browsers block a framed page from navigating your page without a click. It shows a Return to merchant link to your success_url instead, which opens in the whole tab. Most integrations navigate on their own after step 4, so the customer rarely needs it.

Checklist

  • The session is created on your server; the browser only gets its id and url.
  • The iframe has no sandbox; or, with sandbox, the four tokens above, and a 3-D Secure test payment works in your browsers.
  • Your listener checks event.origin, event.source, type and checkout_session.
  • Fulfilment happens only after GET /v1/checkout/sessions/{id} shows payment_status: "paid", and only once per session.
  • success_url and cancel_url work as full pages (3-D Secure returns there).