Use this file to discover all available pages before exploring further.
PayPal’s sandboxed <iframe> integration provides enhanced security by isolating payment processing code within a secure <iframe> wrapper while maintaining seamless communication with the parent merchant page through the postMessage API. In this case, the term “sandboxed” refers to the isolation of payment processing in an <iframe> wrapper, not the sandbox testing environment that PayPal provides.In a sandboxed <iframe> integration, the parent page is a merchant page. Within the merchant page, an <iframe> wrapper uses the PayPal JavaScript SDK v6 and communicates with a server that accesses the PayPal server SDK. The merchant page and <iframe> use postMessage to communicate about the state of a payment. Using the PayPal SDK inside an <iframe> wrapper prevents having to request the SDK script directly from the merchant page.This type of integration is recommended for high-security uses, such as banking and financial services or integrations with strict compliance requirements, such as PCI DSS, or SOC 2. However, it requires more complex setup and maintenance than other PayPal integrations.This implementation consists of two separate applications:
A merchant page at localhost:3001 for your main website and app
A PayPal <iframe> at localhost:3000 that provides an isolated payment processing environment with secure cross-frame communication through the postMessage API
This page provides examples that you can use to help you understand and set up this kind of integration.
Set up yor merchant page HTML before setting up your <iframe> wrapper. This example shows how to set up the main merchant page with proper <iframe> sandbox attributes, overlay containers for modal presentation, and debug information displays.
This example implements page-state management, secure postMessage listeners with origin validation, and handlers for different presentation modes (popup, modal, payment-handler).
class PageState { state = { presentationMode: null, lastPostMessage: null, merchantDomain: null, }; constructor() { this.merchantDomain = window.location.origin; } set presentationMode(value) { this.state.presentationMode = value; const element = document.getElementById("presentationMode"); element.innerHTML = value; } set lastPostMessage(event) { const statusContainer = document.getElementById("postMessageStatus"); statusContainer.innerHTML = JSON.stringify(event.data); this.state.lastPostMessage = event; }}const pageState = new PageState();// Set up secure postMessage listener with origin validationfunction setupPostMessageListener() { window.addEventListener("message", (event) => { // 🔒 CRITICAL: Always validate origin to prevent XSS attacks! if (event.origin !== "http://localhost:3000") { return; } pageState.lastPostMessage = event; const { eventName, data } = event.data; const { presentationMode } = pageState; if (eventName === "presentationMode-changed") { const { presentationMode } = data; pageState.presentationMode = presentationMode; } else if (presentationMode === "popup") { popupPresentationModePostMessageHandler(event); } else if (presentationMode === "modal") { modalPresentationModePostMessageHandler(event); } });}
This example creates the <iframe> content with configuration options for the presentation mode, the PayPal button element, and proper script loading for the v6 SDK.
This JavaScript runs inside the <iframe>, not the merchant page. It initializes the SDK, creates the payment session, and sends status updates back to the merchant page using postMessage.
The v6 SDK script in the <iframe> HTML calls onPayPalWebSdkLoaded() when it finishes loading. This function initializes the SDK and passes the instance to configurePayPalButton().
Replace "YOUR_CLIENT_ID" with your client ID.
Partners: replace "SELLER_MERCHANT_ID" with the merchant ID of the seller you’re creating the payment session for.
configurePayPalButton() uses the SDK instance to create a payment session and attach a click handler to the PayPal button. When the buyer completes, cancels, or triggers an error in the payment flow, the function sends a postMessage event to the merchant page.
This example configures npm scripts to run the merchant page and the PayPal <iframe> servers concurrently using the Vite build tool with proper port assignments. For more information about Vite, see Vite configuration files.
This example sets up separate Vite configurations for the merchant page (port 3001) and PayPal iFrame (port 3000) with proper proxy settings for PayPal API endpoints.
This example configures <iframe> sandbox attributes to enable necessary permissions while maintaining security isolation with allow-scripts, allow-same-origin, allow-popups, and allow-forms.
This example implements critical security measures, such as validating message origins to prevent XSS attacks and ensuring that only trusted domains can communicate with your application.
// ✅ ALWAYS validate message originwindow.addEventListener("message", (event) => { if (event.origin !== expectedOrigin) { return; // Ignore messages from unknown origins } // Process trusted message});// ❌ NEVER accept messages without validationwindow.addEventListener("message", (event) => { // Vulnerable to XSS attacks! processMessage(event.data);});
Define standardized message formats for iframe-to-parent communication, including changes to the presentation mode, payment flow events, and error handling, as shown in this example.
You can implement a comprehensive event-handling system with custom logic for payment start, success, cancellation, and error scenarios. This example also includes analytics tracking and user feedback.
function setupAdvancedPostMessageListener() { window.addEventListener("message", (event) => { if (event.origin !== "http://localhost:3000") { return; } const { eventName, data } = event.data; switch (eventName) { case "payment-flow-start": handlePaymentStart(data); break; case "payment-flow-approved": handlePaymentSuccess(data); break; case "payment-flow-canceled": handlePaymentCancellation(data); break; case "payment-flow-error": handlePaymentError(data); break; case "presentationMode-changed": handlePresentationModeChange(data); break; default: console.warn("Unknown event:", eventName); } });}function handlePaymentSuccess(data) { // Hide loading state hideLoadingIndicator(); // Show success message showSuccessMessage("Payment completed successfully!"); // Redirect or update UI setTimeout(() => { window.location.href = "/success"; }, 2000); // Track conversion trackEvent("payment_completed", data);}
The IframeManager class can handle message queuing, ready-state management, and cleanup operations for better control over <iframe> lifecycle, as shown in this example.
Implement payment retry management with configurable retry limits, user confirmation dialogs, and graceful fallback handling for failed payment attempts, as shown in this example.
class PaymentRetryManager { constructor(maxRetries = 3) { this.maxRetries = maxRetries; this.currentRetries = 0; } async handlePaymentError(error, retryCallback) { console.error("Payment error:", error); if (this.currentRetries < this.maxRetries) { this.currentRetries++; // Show retry option to user const shouldRetry = await showRetryDialog( `Payment failed (${this.currentRetries}/${this.maxRetries}). Would you like to try again?` ); if (shouldRetry) { return retryCallback(); } } // Max retries reached or user declined retry this.currentRetries = 0; showFinalErrorMessage("Payment could not be completed. Please try a different payment method."); } reset() { this.currentRetries = 0; }}const retryManager = new PaymentRetryManager();
In a production environment, configure Express.js security headers for production deployment, including X-Frame-Options, content security policy (CSP), and MIME-type protection. For more information about CSP, see Content security policy header configuration.
To troubleshoot common postMessage communication issues, check origin validation and <iframe> loading status. Also, confirm that you configured the proper sandbox attributes.