Skip to main content

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.

ContainerExecution modeIntegration
Website in a browserEMBEDDEDMount the returned URL with the browser helper
React Native WebView loading Hubpay directlyEMBEDDEDLoad 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 directlyEMBEDDEDInstall the named, origin-restricted bridge before loading the returned URL
Native WebView loading your HTTPS page, with Hubpay inside that pageEMBEDDEDMount with the browser helper and relay its events to the app
Salesforce Lightning or Experience Cloud pageEMBEDDEDFollow the Salesforce guidance below
Chrome Custom Tab or SFSafariViewControllerHOSTED_PAGEOpen 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() and checkout.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:

PlatformBridge received by Hubpay
Native AndroidA WebView message listener named hubpayCheckout
Native iOSA WKScriptMessageHandler named hubpayCheckout
React NativeA 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:

  1. Add the Hubpay payment-portal hostname as a Salesforce Trusted URL for frame content.
  2. Mount the returned paymentUrl in an iframe with allow="payment".
  3. 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 by this.template.querySelector(...) to mount; a global selector cannot reach into the component's shadow DOM.
  4. If the Salesforce surface places your component inside another iframe, confirm that frame also delegates the payment capability. Otherwise use the hosted payment page for Apple Pay or Google Pay.
  5. 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:step events, because that attempt may still complete.
  • Expired, when the request has become EXPIRED, on every screen. This is the same moment the v1.collection.payment_request.expired webhook 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;
}
});
EventDataMeaning
hubpay:readyNoneThe embedded frame is running
hubpay:stepstep, paid, remainingThe payer reached a checkout step
hubpay:successpaid, remainingThe payer completed the checkout journey
hubpay:expiredpaid, remainingThe request expired, or stopped accepting new payments, before it was paid in full (see Expiry)
hubpay:errorOptional messageThe checkout reached a terminal error state
hubpay:cancelNoneThe 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​

ParameterValueEffect
embeddedtrueReturned automatically for EMBEDDED payment requests
originImmediate host page originRequired only for an iframe; added automatically by the browser helper

Optional parameters​

ParameterValueEffect
primaryHex colourOverrides the primary action colour
accentHex colourOverrides highlights such as focus and countdown marks
backgroundHex colourPaints the checkout surface; the frame is transparent when omitted
themedarkSelects the embedded dark theme
navhostLets the parent page control Back navigation
footerhiddenHides the embedded compliance footer when your integration owns that content

See Payment page branding for colour behaviour and accessibility adjustments.

Next steps​