Embedded checkout
Use embedded checkout when your website or native app owns the surrounding payment journey and needs Hubpay to look and behave like part of that experience.
EMBEDDED is the presentation mode for both browser iframes and direct native WebViews. The container changes how lifecycle events and Back navigation cross the boundary; it does not change the execution mode.
| Container | Execution mode | Integration |
|---|---|---|
| Website in a browser | EMBEDDED | Mount the returned URL with the browser helper |
| React Native WebView loading Hubpay directly | EMBEDDED | Load the returned URL and expose the React Native container signal; use a verified native bridge if the app owns events or Back navigation |
| Native iOS or Android WebView loading Hubpay directly | EMBEDDED | Install the named, origin-restricted bridge before loading the returned URL |
| Native WebView loading your HTTPS page, with Hubpay inside that page | EMBEDDED | Mount with the browser helper and relay its events to the app |
| Salesforce Lightning or Experience Cloud page | EMBEDDED | Follow the Salesforce guidance below |
Chrome Custom Tab or SFSafariViewController | HOSTED_PAGE | Open the returned payment URL directly |
Create a payment request with executionMode set to EMBEDDED. The returned paymentUrl is already marked for embedded rendering and retains any access token required by the checkout.
Account-level payment page branding applies automatically. You do not need to send colours or an embedding origin when creating each payment request.
Set the executionMode field to EMBEDDED alongside the payment-request details described in the API reference.
Part of the response:
{
"paymentUrl": "https://pay.hubpay.ae/AH-VGTJ-24GX?sessionToken=eyJhbGciOiJSUzI1NiIs...&embedded=true"
}
Use the response URL inside your website or app. If you also ask Hubpay to email the payment request, the emailed link uses the standalone hosted presentation because an email recipient has no merchant container; the API response URL remains embedded.
Browser integration
Load hubpay-checkout.js from the same payment-portal environment as the returned paymentUrl, then pass the URL to the helper. For production:
<script src="https://pay.hubpay.ae/hubpay-checkout.js"></script>
<div id="hubpay-checkout"></div>
<script>
const checkout = HubpayCheckout.mount({
paymentUrl: paymentRequest.paymentUrl,
element: "#hubpay-checkout",
onEvent(event) {
if (event.type === "hubpay:success") {
// Update the browser journey, then confirm payment server-side.
} else if (event.type === "hubpay:expired") {
// The request ran out of time: close the frame (see Expiry).
}
},
});
</script>
In non-production environments, load hubpay-checkout.js from the hostname in that environment's returned paymentUrl.
The helper:
- Preserves the complete returned URL, including its access parameters
- Adds the current page's
window.location.origin - Mounts an iframe with
allow="payment"and safe layout defaults - Accepts messages only from the mounted checkout window and Hubpay origin
- Exposes
checkout.back()andcheckout.destroy()lifecycle controls
How embedded mode starts
The EMBEDDED execution mode adds embedded=true to the returned payment URL. Hubpay honours that explicit mode in an iframe or when the top-level document can detect a native app container. React Native supplies the container signal when onMessage is configured; native iOS and Android supply it through the named bridge described below.
Embedded checkout:
- Removes the merchant identity header and hosted-page chrome
- Skips the initial Start page and opens payment-method selection
- Skips payment-method selection as well when the payment request has only one available method
- Reports checkout progress through the browser parent or native message bridge
- Lets a connected iframe or native host own completion instead of navigating to a hosted return page
Do not distribute an EMBEDDED URL as a standalone browser payment link. If it opens in a normal top-level browser tab without a native container, Hubpay deliberately falls back to the full hosted page, including merchant identity, Back control and completion redirects. Create the payment request with HOSTED_PAGE when the payer is intended to navigate to a standalone Hubpay page.
The frame has a minimum supported width of 360 px. Add allow="payment" so browser wallet payment methods can work when available.
If checkout is nested inside more than one iframe, every iframe ancestor must permit the browser's payment capability. A child frame cannot re-enable a capability blocked by one of its parents. If you do not control every ancestor, open the hosted payment page at the top level for Apple Pay or Google Pay rather than promising that those methods will be available inside the nested frame. See the browser's Permissions Policy inheritance rules.
Manual iframe integration
Use this only when the browser helper cannot be loaded. Preserve the returned URL and its access parameters. Add the runtime origin with the browser URL API instead of rebuilding the URL from a hostname or payment reference:
const checkoutUrl = new URL(paymentUrl);
checkoutUrl.searchParams.set("origin", window.location.origin);
const iframe = document.querySelector("#hubpay-checkout");
iframe.src = checkoutUrl.toString();
<iframe
id="hubpay-checkout"
title="Payment checkout"
allow="payment"
width="100%"
height="720"
></iframe>
Set the host origin
origin is the exact origin of the page containing the iframe. It consists of the scheme, hostname and port, when present, with no path:
https://merchant.example
https://merchant.example:8443
Hubpay uses this value as the permitted destination for checkout events and to validate host-driven navigation messages. Using window.location.origin with URLSearchParams, as in the example above, supplies and encodes the correct value.
If origin is absent or invalid, the checkout still uses compact presentation and the payer can continue, but it is not treated as host-connected. Hubpay keeps its own Back control and completion redirects, while outbound events such as hubpay:ready and hubpay:success are dropped. Hubpay never broadcasts payment events to every framing page.
The embedding page must be served over HTTPS in shared and production environments. Exact http://localhost origins are accepted for local development only; other plain-HTTP hosts are not connected and do not receive checkout events. An HTTPS development origin or tunnel remains the closest test of production behaviour.
Do not mount embedded checkout from a file:, data: or other locally generated document. Those documents have an opaque null origin, so Hubpay cannot restrict payment events to a verifiable recipient. The same limitation applies when an ancestor sandboxes the wrapper page without allow-same-origin. Host the wrapper page at an HTTPS URL and ensure it keeps its normal web origin.
Native apps and nested WebViews
Direct native WebView
Load the API's returned paymentUrl unchanged. It already contains embedded=true; a direct WebView does not need origin because it has no browser parent page. Native iOS and Android apps must install the verified bridge before loading the URL so Hubpay can recognise the app container from its first render. React Native apps can use the lightweight container signal in the mobile guide when the app does not consume events.
Hubpay sends the same lifecycle events as JSON through a named, origin-checked bridge:
| Platform | Bridge received by Hubpay |
|---|---|
| Native Android | A WebView message listener named hubpayCheckout |
| Native iOS | A WKScriptMessageHandler named hubpayCheckout |
| React Native | A custom native wrapper exposing the same verified hubpayCheckout bridge |
Install the bridge only for the allowlisted Hubpay payment-portal origin and accept messages only from the main frame. Parse the message as a checkout event, update the app journey, and still confirm the final payment on your backend.
If your native screen owns the Back button, add nav=host to the returned URL and inject this command into the Hubpay main document when the payer taps Back:
window.postMessage(
JSON.stringify({ type: "hubpay:back" }),
window.location.origin,
);
Use React Native's injectJavaScript, Android's evaluateJavascript, or iOS evaluateJavaScript to run that command. Do not set nav=host until this path works, because it intentionally hides Hubpay's Back control.
If your React Native WebView uses enableApplePay, leave nav unset and keep Hubpay's Back control. That mode disables injectJavaScript, so the app cannot reliably send the host-owned Back command.
See Mobile apps and WebViews for platform setup and wallet-app navigation.
Merchant wrapper with a child iframe
origin always identifies the immediate web page that contains the Hubpay iframe. It is not the native app's bundle identifier, custom URL scheme, the top-level frame's origin or the Hubpay origin. The helper gets this right by using its own window.location.origin at runtime.
Hubpay sends browser events only to that immediate parent page. Browser events do not automatically cross another iframe boundary or enter a native message bridge. If a native app needs checkout events, relay the helper callback explicitly.
For a merchant HTTPS wrapper running inside a native WebView, relay the browser helper's already-validated event through the named bridge:
<script>
const nativeBridge =
window.hubpayCheckout ??
window.webkit?.messageHandlers?.hubpayCheckout;
HubpayCheckout.mount({
paymentUrl,
element: "#hubpay-checkout",
onEvent(event) {
nativeBridge?.postMessage(JSON.stringify(event));
},
});
</script>
For native iOS, verify the message is from the wrapper page's HTTPS origin and main frame in a named WKScriptMessageHandler. For native Android, expose hubpayCheckout only to the wrapper origin with WebViewCompat.addWebMessageListener and require isMainFrame. A React Native integration needs a small native wrapper around one of those verified handlers; do not forward these events through generic onMessage, broadcast with *, or expose a general-purpose JavaScript interface to untrusted pages.
The outer native WebView still owns navigation to wallet apps, pop-ups and new windows, but native navigation callbacks are not a portable guarantee for every child-frame navigation. Prefer the direct EMBEDDED WebView when the wrapper adds no essential merchant content. Use the HTTPS-wrapper pattern only after validating every enabled method in the actual app and operating-system versions you support.
Follow Mobile apps and WebViews as well as this guide when an HTTPS wrapper page runs inside a native app.
Salesforce
For Lightning Web Components and Experience Cloud, use EMBEDDED, but apply Salesforce's own browser restrictions:
- Add the Hubpay payment-portal hostname as a Salesforce Trusted URL for frame content.
- Mount the returned
paymentUrlin an iframe withallow="payment". - If you use
hubpay-checkout.js, upload the file as a Salesforce static resource and load it with Salesforce's resource loader. Lightning Content Security Policy does not allow an arbitrary remote<script>tag or inline JavaScript. In an LWC, pass the element returned bythis.template.querySelector(...)tomount; a global selector cannot reach into the component's shadow DOM. - If the Salesforce surface places your component inside another iframe, confirm that frame also delegates the
paymentcapability. Otherwise use the hosted payment page for Apple Pay or Google Pay. - In a Mobile Publisher app, separately allowlist the crypto wallet URL schemes and test them in a real branded build. Publisher Playground does not support outbound custom URL schemes.
The manual iframe integration remains available when packaging the helper as a static resource is not practical.
Expiry
An EMBEDDED payment request runs on the same clock as a hosted one: expiresAt is 60 minutes after creation, the payer can start a payment for the first 45 minutes, and the last 15 minutes let funds already sent arrive. If the payer never took a quote or started a card or bank payment by the 45 minute mark the request expires then; otherwise an unpaid request expires at expiresAt, except that a deposit sent in time but still confirming holds it open, after which the request is PAID if the deposit covers the balance and expires otherwise. When a request expires it becomes EXPIRED and v1.collection.payment_request.expired is sent with amountPaid and currency. See Payment request events.
Inside the frame, a quote taken late runs only until the 45 minute mark: a payer who reaches the quote screen 15 minutes after the request was created sees a 30 minute quote, one who reaches it at 40 minutes sees a 5 minute quote. After the 45 minute mark no new payment attempt can start. Design the surrounding journey so a payer who has run out of time is offered a fresh payment request rather than left in the frame: create a new request and mount its paymentUrl. Treat the webhook or the API status as the signal that a request has expired; the embedded session token cannot outlive expiresAt.
The frame tells your page when this happens with a hubpay:expired event (see Receive checkout events). It is sent once for each of two states, and no hubpay:step follows it:
- Closed for new payments, at the 45 minute mark, when the frame is showing a screen from which no payment is under way: the payment method choice, or any crypto step up to and including the quote screen (a displayed quote ends at the 45 minute mark, so it closes with the request). A card payment or bank transfer the payer had already started, or a crypto deposit already reported as sent, keeps its screen and its
hubpay:stepevents, because that attempt may still complete. - Expired, when the request has become
EXPIRED, on every screen. This is the same moment thev1.collection.payment_request.expiredwebhook is sent.
The frame then shows the payer a closed or expired message with no way forward, so close the frame or return to your own journey. On expired the request is final and you can mount a replacement straight away. On closed the request is still open for up to 15 minutes: a deposit the payer sent before navigating back may still be confirming, and the request becomes PAID if it arrives. Before offering a replacement after a closed event, confirm through the webhook or the API that the request has reached EXPIRED, or you may collect the same amount twice. paid and remaining are the frame's latest known totals and may be up to one poll (30 seconds) behind; use the webhook or the API for the amounts.
Receive checkout events
In a direct native WebView, these events arrive as a JSON string through the native bridge described above. In an iframe, the browser helper performs the security checks automatically. For a manual iframe integration, listen on the parent page and verify both the checkout origin and the sending window before using a message:
const iframe = document.querySelector("#hubpay-checkout");
const checkoutOrigin = new URL(paymentUrl).origin;
window.addEventListener("message", (event) => {
if (event.origin !== checkoutOrigin) return;
if (event.source !== iframe.contentWindow) return;
if (!event.data || typeof event.data !== "object") return;
switch (event.data.type) {
case "hubpay:ready":
// The frame is ready.
break;
case "hubpay:step":
// Read event.data.step, event.data.paid and event.data.remaining.
break;
case "hubpay:success":
// Update the browser journey, then confirm payment server-side.
break;
case "hubpay:expired":
// The request expired, or stopped accepting new payments, before it
// was paid in full. Close the frame; see Expiry before offering a
// replacement.
break;
case "hubpay:error":
// The checkout reached a terminal error state.
break;
case "hubpay:cancel":
// Close the embedded journey or return to your previous screen.
break;
}
});
| Event | Data | Meaning |
|---|---|---|
hubpay:ready | None | The embedded frame is running |
hubpay:step | step, paid, remaining | The payer reached a checkout step |
hubpay:success | paid, remaining | The payer completed the checkout journey |
hubpay:expired | paid, remaining | The request expired, or stopped accepting new payments, before it was paid in full (see Expiry) |
hubpay:error | Optional message | The checkout reached a terminal error state |
hubpay:cancel | None | The payer backed out of the first embedded step |
Browser events control the customer journey; they are not server-side proof that funds were received. Confirm payment status from a signed webhook or API lookup before fulfilment or accounting actions.
Let the host control Back
Add nav=host only when your containing page or native screen supplies a working Back control. Hubpay then hides the checkout Back control. In a direct WebView, inject the command shown under Direct native WebView. With the browser helper, call checkout.back(). For a manual iframe integration, send hubpay:back to the checkout frame:
iframe.contentWindow?.postMessage(
{ type: "hubpay:back" },
new URL(paymentUrl).origin,
);
Required parameters
| Parameter | Value | Effect |
|---|---|---|
embedded | true | Returned automatically for EMBEDDED payment requests |
origin | Immediate host page origin | Required only for an iframe; added automatically by the browser helper |
Optional parameters
| Parameter | Value | Effect |
|---|---|---|
primary | Hex colour | Overrides the primary action colour |
accent | Hex colour | Overrides highlights such as focus and countdown marks |
background | Hex colour | Paints the checkout surface; the frame is transparent when omitted |
theme | dark | Selects the embedded dark theme |
nav | host | Lets the parent page control Back navigation |
footer | hidden | Hides the embedded compliance footer when your integration owns that content |
See Payment page branding for colour behaviour and accessibility adjustments.
Next steps
- Mobile apps and WebViews — configure a direct embedded WebView, browser sheet or nested wrapper
- Create a hosted payment page
- Payment lifecycle and webhooks
- Preview embedded checkout