Sandbox the v6 SDK
Last updated: Aug 1st, 11:05am
Overview
PayPal's sandboxed iframe integration provides enhanced security by isolating payment processing code within a secure iframe environment while maintaining seamless communication with the parent merchant page through postMessage API.
When to Use Sandboxed Iframe Integration
Recommended for high security requirements like banking and financial services, strict CSP policies in enterprise applications, compliance needs (PCI DSS, SOC 2), and micro-frontend architectures with isolated payment components.
Integration Benefits and Limitations
Provides enhanced security through isolation, meets strict compliance standards, offers flexibility with different domains, but adds complexity in setup and maintenance compared to standard PayPal integration methods.
Architecture
The implementation consists of two separate applications: a Merchant Page (localhost:3001) for your main website/application and a PayPal Iframe (localhost:3000) for isolated payment processing environment with secure cross-frame communication via postMessage.
Prerequisites
Before integrating, create a PayPal developer account and obtain Client ID and Secret from the PayPal Developer Dashboard, ensure your PayPal server provides required endpoints, and set up environment configuration with proper credentials.
Environment Configuration
Create a .env file with your PayPal sandbox credentials to enable proper authentication and API communication.
1PAYPAL_SANDBOX_CLIENT_ID=your_client_id2PAYPAL_SANDBOX_CLIENT_SECRET=your_client_secret
Merchant Page HTML Structure
Set up the main merchant page with proper iframe sandbox attributes, overlay containers for modal presentation, and debug information displays.
1<!doctype html>2<html lang="en">3 <head>4 <meta charset="UTF-8" />5 <title>PayPal iframe Example</title>6 <meta name="viewport" content="width=device-width, initial-scale=1" />7 <style>8 #iframeWrapper {9 border: none;10 width: 100%;11 height: 300px;12 }1314 #iframeWrapper.fullWindow {15 position: absolute;16 top: 0;17 left: 0;18 height: 100%;19 width: 100%;20 }2122 #overlayContainer::backdrop {23 background: rgba(0, 0, 0, 0.67);24 }25 </style>26 </head>27 <body>28 <script async src="/app.js" onload="onLoad()"></script>2930 <div id="mainContainer">31 <h1>PayPal iframe Example</h1>3233 <!-- Sandboxed iframe with proper permissions -->34 <iframe35 id="iframeWrapper"36 src="http://localhost:3000/?origin=http://localhost:3001"37 sandbox="allow-scripts allow-same-origin allow-popups allow-forms"38 allow="payment"39 ></iframe>40 </div>4142 <!-- Modal overlay for popup presentation mode -->43 <dialog id="overlayContainer">44 <div id="overlayContainerLogo">45 <img src="https://www.paypalobjects.com/js-sdk-logos/2.2.7/paypal-white.svg" />46 </div>47 <button id="overlayCloseButtonCTA">Close</button>48 <button id="overlayCloseButtonBackdrop">X</button>49 </dialog>50 </body>51</html>
Merchant Page JavaScript Implementation
Implement page state management, secure postMessage listeners with origin validation, and handlers for different presentation modes (popup, modal, payment-handler).
1class PageState {2 state = {3 presentationMode: null,4 lastPostMessage: null,5 merchantDomain: null,6 };78 constructor() {9 this.merchantDomain = window.location.origin;10 }1112 set presentationMode(value) {13 this.state.presentationMode = value;14 const element = document.getElementById("presentationMode");15 element.innerHTML = value;16 }1718 set lastPostMessage(event) {19 const statusContainer = document.getElementById("postMessageStatus");20 statusContainer.innerHTML = JSON.stringify(event.data);21 this.state.lastPostMessage = event;22 }23}2425const pageState = new PageState();2627// Setup secure postMessage listener with origin validation28function setupPostMessageListener() {29 window.addEventListener("message", (event) => {30 // 🔒 CRITICAL: Always validate origin to prevent XSS attacks!31 if (event.origin !== "http://localhost:3000") {32 return;33 }3435 pageState.lastPostMessage = event;36 const { eventName, data } = event.data;37 const { presentationMode } = pageState;3839 if (eventName === "presentationMode-changed") {40 const { presentationMode } = data;41 pageState.presentationMode = presentationMode;42 } else if (presentationMode === "popup") {43 popupPresentationModePostMessageHandler(event);44 } else if (presentationMode === "modal") {45 modalPresentationModePostMessageHandler(event);46 }47 });48}
PayPal Iframe HTML Structure
Create the iframe content with presentation mode configuration options, PayPal button element, and proper script loading for the PayPal v6 Web SDK.
1<!doctype html>2<html lang="en">3 <head>4 <meta charset="UTF-8" />5 <title>PayPal Payment Handler</title>6 <meta name="viewport" content="width=device-width, initial-scale=1" />7 <style>8 body {9 font-family: sans-serif;10 color: #e5f5ea;11 background: #334037;12 }13 </style>14 </head>15 <body>16 <!-- Presentation mode configuration -->17 <div id="config">18 <div>Payment flow config</div>19 <fieldset>20 <legend>presentationMode</legend>21 <div>22 <input23 checked24 type="radio"25 id="presentationMode-popup"26 name="presentationMode"27 value="popup"28 />29 <label for="presentationMode-popup">popup</label>30 </div>31 <div>32 <input33 type="radio"34 id="presentationMode-modal"35 name="presentationMode"36 value="modal"37 />38 <label for="presentationMode-modal">modal</label>39 </div>40 </fieldset>41 </div>4243 <!-- PayPal button -->44 <paypal-button id="paypal-button"></paypal-button>4546 <script src="/integration.js"></script>47 <script48 async49 src="https://www.sandbox.paypal.com/web-sdk/v6/core"50 onload="onPayPalWebSdkLoaded()"51 ></script>52 </body>53</html>
PayPal Iframe JavaScript Implementation
Implement secure communication with parent window, PayPal SDK initialization, payment session creation, and event handlers for payment flow states.
1class PageState {2 state = {3 paymentSession: null,4 };56 get paymentSession() {7 return this.state.paymentSession;8 }910 set paymentSession(value) {11 this.state.paymentSession = value;12 }13}1415const pageState = new PageState();1617// Setup PayPal button with payment session18async function setupPayPalButton(sdkInstance) {19 pageState.paymentSession = sdkInstance.createPayPalOneTimePaymentSession({20 onApprove: async (data) => {21 const orderData = await captureOrder({22 orderId: data.orderId,23 });2425 sendPostMessageToParent({26 eventName: "payment-flow-approved",27 data: orderData,28 });29 },30 onCancel: (data) => {31 sendPostMessageToParent({32 eventName: "payment-flow-canceled",33 data: {34 orderId: data?.orderId,35 },36 });37 },38 onError: (data) => {39 sendPostMessageToParent({40 eventName: "payment-flow-error",41 data: {42 orderId: data?.orderId,43 },44 });45 },46 });4748 const paypalButton = document.querySelector("#paypal-button");49 paypalButton.addEventListener("click", async () => {50 const paymentFlowConfig = {51 presentationMode: getSelectedPresentationMode(),52 fullPageOverlay: { enabled: false },53 };5455 sendPostMessageToParent({56 eventName: "payment-flow-start",57 data: { paymentFlowConfig },58 });5960 try {61 await pageState.paymentSession.start(paymentFlowConfig, createOrder());62 } catch (e) {63 console.error("Payment start error:", e);64 sendPostMessageToParent({65 eventName: "payment-flow-error",66 data: { error: e.message },67 });68 }69 });70}
Package.json Configuration
Configure npm scripts to run both merchant page and PayPal iframe servers concurrently using Vite build tool with proper port assignments.
1{2 "name": "v6-web-sdk-sample-html-iframe",3 "version": "1.0.0",4 "description": "PayPal sandboxed iframe integration example",5 "scripts": {6 "merchant-page": "vite --config vite-merchant-example.config.js",7 "paypal-iframe": "vite --config vite-paypal-iframe.config.js",8 "start": "concurrently \"npm run merchant-page\" \"npm run paypal-iframe\"",9 "format": "prettier . --write",10 "format:check": "prettier . --check"11 },12 "dependencies": {13 "concurrently": "^9.1.2",14 "vite": "^7.0.4"15 }16}
Vite Configuration Files
Set up separate Vite configurations for merchant page (port 3001) and PayPal iframe (port 3000) with proper proxy settings for PayPal API endpoints.
1// vite-merchant-example.config.js2import { defineConfig } from "vite";34export default defineConfig({5 plugins: [],6 root: "src/merchant-example",7 server: {8 port: 3001,9 },10});1112// vite-paypal-iframe.config.js13import { defineConfig } from "vite";1415export default defineConfig({16 plugins: [],17 root: "src/paypal-iframe",18 server: {19 port: 3000,20 proxy: {21 "/paypal-api": {22 target: "http://localhost:8080",23 changeOrigin: true,24 secure: false,25 },26 },27 },28});
Iframe Sandbox Security
Configure iframe sandbox attributes to enable necessary permissions while maintaining security isolation with allow-scripts, allow-same-origin, allow-popups, and allow-forms.
1<iframe2 src="http://localhost:3000/?origin=http://localhost:3001"3 sandbox="allow-scripts allow-same-origin allow-popups allow-forms"4 allow="payment"5></iframe>
PostMessage Origin Validation
Implement critical security measures by validating message origins to prevent XSS attacks and ensure only trusted domains can communicate with your application.
1// ✅ ALWAYS validate message origin2window.addEventListener("message", (event) => {3 if (event.origin !== expectedOrigin) {4 return; // Ignore messages from unknown origins5 }6 // Process trusted message7});89// ❌ NEVER accept messages without validation10window.addEventListener("message", (event) => {11 // Vulnerable to XSS attacks!12 processMessage(event.data);13});
Content Security Policy Configuration
Set up CSP headers to control resource loading and prevent unauthorized script execution while allowing necessary PayPal SDK and iframe communication.
1<meta http-equiv="Content-Security-Policy" content="2 default-src 'self';3 script-src 'self' 'unsafe-inline' https://www.sandbox.paypal.com;4 frame-src 'self' http://localhost:3000;5 connect-src 'self' http://localhost:8080;6 img-src 'self' https://www.paypalobjects.com;7">
Communication Protocol Messages
Define standardized message formats for iframe-to-parent communication including presentation mode changes, payment flow events, and error handling.
1// Presentation mode changed2{3 eventName: "presentationMode-changed",4 data: { presentationMode: "popup" }5}67// Payment flow started8{9 eventName: "payment-flow-start",10 data: { paymentFlowConfig: {...} }11}1213// Payment approved14{15 eventName: "payment-flow-approved",16 data: { orderId: "...", captureData: {...} }17}1819// Payment canceled20{21 eventName: "payment-flow-canceled",22 data: { orderId: "..." }23}2425// Payment error26{27 eventName: "payment-flow-error",28 data: { error: "..." }29}
Advanced Event Handling
Implement comprehensive event handling system with custom logic for payment start, success, cancellation, and error scenarios including analytics tracking and user feedback.
1function setupAdvancedPostMessageListener() {2 window.addEventListener("message", (event) => {3 if (event.origin !== "http://localhost:3000") {4 return;5 }67 const { eventName, data } = event.data;89 switch (eventName) {10 case "payment-flow-start":11 handlePaymentStart(data);12 break;13 case "payment-flow-approved":14 handlePaymentSuccess(data);15 break;16 case "payment-flow-canceled":17 handlePaymentCancellation(data);18 break;19 case "payment-flow-error":20 handlePaymentError(data);21 break;22 case "presentationMode-changed":23 handlePresentationModeChange(data);24 break;25 default:26 console.warn("Unknown event:", eventName);27 }28 });29}3031function handlePaymentSuccess(data) {32 // Hide loading state33 hideLoadingIndicator();3435 // Show success message36 showSuccessMessage("Payment completed successfully!");3738 // Redirect or update UI39 setTimeout(() => {40 window.location.href = "/success";41 }, 2000);4243 // Track conversion44 trackEvent("payment_completed", data);45}
Dynamic Iframe Management
Create an iframe manager class to handle message queuing, ready state management, and cleanup operations for better control over iframe lifecycle.
1class IframeManager {2 constructor(iframeId, targetOrigin) {3 this.iframe = document.getElementById(iframeId);4 this.targetOrigin = targetOrigin;5 this.messageQueue = [];6 this.isReady = false;7 }89 sendMessage(payload) {10 if (this.isReady) {11 this.iframe.contentWindow.postMessage(payload, this.targetOrigin);12 } else {13 this.messageQueue.push(payload);14 }15 }1617 onReady() {18 this.isReady = true;19 // Send queued messages20 this.messageQueue.forEach(payload => {21 this.iframe.contentWindow.postMessage(payload, this.targetOrigin);22 });23 this.messageQueue = [];24 }2526 destroy() {27 this.iframe.remove();28 this.isReady = false;29 this.messageQueue = [];30 }31}3233const iframeManager = new IframeManager("iframeWrapper", "http://localhost:3000");
Error Recovery and Retry Logic
Implement payment retry management with configurable retry limits, user confirmation dialogs, and graceful fallback handling for failed payment attempts.
1class PaymentRetryManager {2 constructor(maxRetries = 3) {3 this.maxRetries = maxRetries;4 this.currentRetries = 0;5 }67 async handlePaymentError(error, retryCallback) {8 console.error("Payment error:", error);910 if (this.currentRetries < this.maxRetries) {11 this.currentRetries++;1213 // Show retry option to user14 const shouldRetry = await showRetryDialog(15 `Payment failed (${this.currentRetries}/${this.maxRetries}). Would you like to try again?`16 );1718 if (shouldRetry) {19 return retryCallback();20 }21 }2223 // Max retries reached or user declined retry24 this.currentRetries = 0;25 showFinalErrorMessage("Payment could not be completed. Please try a different payment method.");26 }2728 reset() {29 this.currentRetries = 0;30 }31}3233const retryManager = new PaymentRetryManager();
Production Security Headers
Configure Express.js security headers for production deployment including X-Frame-Options, Content Security Policy, and MIME type protection.
1// Express.js security headers2app.use((req, res, next) => {3 // Prevent iframe embedding from untrusted domains4 res.setHeader('X-Frame-Options', 'SAMEORIGIN');56 // Content Security Policy7 res.setHeader('Content-Security-Policy', `8 default-src 'self';9 script-src 'self' 'unsafe-inline' https://www.paypal.com;10 frame-src 'self' https://payments.example.com;11 connect-src 'self' https://api.paypal.com;12 img-src 'self' https://www.paypalobjects.com;13 `.replace(/\s+/g, ' ').trim());1415 // Prevent MIME type sniffing16 res.setHeader('X-Content-Type-Options', 'nosniff');1718 next();19});
Troubleshooting PostMessage Issues
Debug common postMessage communication problems by checking origin validation, iframe loading status, and ensuring proper sandbox attributes are configured.
1// Check origin validation2if (event.origin !== expectedOrigin) {3 console.warn(`Message rejected from origin: ${event.origin}`);4 return;5}67// Check iframe loading8iframe.onload = () => {9 console.log("Iframe ready for postMessage communication");10};1112// Ensure proper sandbox attributes13const requiredSandboxAttrs = [14 'allow-scripts',15 'allow-same-origin',16 'allow-popups',17 'allow-forms'18];1920const sandboxAttr = iframe.getAttribute('sandbox');21requiredSandboxAttrs.forEach(attr => {22 if (!sandboxAttr.includes(attr)) {23 console.error(`Missing sandbox attribute: ${attr}`);24 }25});
Additional Resources
Follow this link for an E2E example in our public examples repo on Github.