Sandbox the v6 SDK

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_id
    2PAYPAL_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 }
      13
      14 #iframeWrapper.fullWindow {
      15 position: absolute;
      16 top: 0;
      17 left: 0;
      18 height: 100%;
      19 width: 100%;
      20 }
      21
      22 #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>
      29
      30 <div id="mainContainer">
      31 <h1>PayPal iframe Example</h1>
      32
      33 <!-- Sandboxed iframe with proper permissions -->
      34 <iframe
      35 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>
      41
      42 <!-- 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 };
        7
        8 constructor() {
        9 this.merchantDomain = window.location.origin;
        10 }
        11
        12 set presentationMode(value) {
        13 this.state.presentationMode = value;
        14 const element = document.getElementById("presentationMode");
        15 element.innerHTML = value;
        16 }
        17
        18 set lastPostMessage(event) {
        19 const statusContainer = document.getElementById("postMessageStatus");
        20 statusContainer.innerHTML = JSON.stringify(event.data);
        21 this.state.lastPostMessage = event;
        22 }
        23}
        24
        25const pageState = new PageState();
        26
        27// Setup secure postMessage listener with origin validation
        28function 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 }
        34
        35 pageState.lastPostMessage = event;
        36 const { eventName, data } = event.data;
        37 const { presentationMode } = pageState;
        38
        39 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 <input
          23 checked
          24 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 <input
          33 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>
          42
          43 <!-- PayPal button -->
          44 <paypal-button id="paypal-button"></paypal-button>
          45
          46 <script src="/integration.js"></script>
          47 <script
          48 async
          49 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 };
            5
            6 get paymentSession() {
            7 return this.state.paymentSession;
            8 }
            9
            10 set paymentSession(value) {
            11 this.state.paymentSession = value;
            12 }
            13}
            14
            15const pageState = new PageState();
            16
            17// Setup PayPal button with payment session
            18async function setupPayPalButton(sdkInstance) {
            19 pageState.paymentSession = sdkInstance.createPayPalOneTimePaymentSession({
            20 onApprove: async (data) => {
            21 const orderData = await captureOrder({
            22 orderId: data.orderId,
            23 });
            24
            25 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 });
            47
            48 const paypalButton = document.querySelector("#paypal-button");
            49 paypalButton.addEventListener("click", async () => {
            50 const paymentFlowConfig = {
            51 presentationMode: getSelectedPresentationMode(),
            52 fullPageOverlay: { enabled: false },
            53 };
            54
            55 sendPostMessageToParent({
            56 eventName: "payment-flow-start",
            57 data: { paymentFlowConfig },
            58 });
            59
            60 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.js
                2import { defineConfig } from "vite";
                3
                4export default defineConfig({
                5 plugins: [],
                6 root: "src/merchant-example",
                7 server: {
                8 port: 3001,
                9 },
                10});
                11
                12// vite-paypal-iframe.config.js
                13import { defineConfig } from "vite";
                14
                15export 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<iframe
                  2 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 origin
                    2window.addEventListener("message", (event) => {
                    3 if (event.origin !== expectedOrigin) {
                    4 return; // Ignore messages from unknown origins
                    5 }
                    6 // Process trusted message
                    7});
                    8
                    9// ❌ NEVER accept messages without validation
                    10window.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 changed
                        2{
                        3 eventName: "presentationMode-changed",
                        4 data: { presentationMode: "popup" }
                        5}
                        6
                        7// Payment flow started
                        8{
                        9 eventName: "payment-flow-start",
                        10 data: { paymentFlowConfig: {...} }
                        11}
                        12
                        13// Payment approved
                        14{
                        15 eventName: "payment-flow-approved",
                        16 data: { orderId: "...", captureData: {...} }
                        17}
                        18
                        19// Payment canceled
                        20{
                        21 eventName: "payment-flow-canceled",
                        22 data: { orderId: "..." }
                        23}
                        24
                        25// Payment error
                        26{
                        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 }
                          6
                          7 const { eventName, data } = event.data;
                          8
                          9 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}
                          30
                          31function handlePaymentSuccess(data) {
                          32 // Hide loading state
                          33 hideLoadingIndicator();
                          34
                          35 // Show success message
                          36 showSuccessMessage("Payment completed successfully!");
                          37
                          38 // Redirect or update UI
                          39 setTimeout(() => {
                          40 window.location.href = "/success";
                          41 }, 2000);
                          42
                          43 // Track conversion
                          44 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 }
                            8
                            9 sendMessage(payload) {
                            10 if (this.isReady) {
                            11 this.iframe.contentWindow.postMessage(payload, this.targetOrigin);
                            12 } else {
                            13 this.messageQueue.push(payload);
                            14 }
                            15 }
                            16
                            17 onReady() {
                            18 this.isReady = true;
                            19 // Send queued messages
                            20 this.messageQueue.forEach(payload => {
                            21 this.iframe.contentWindow.postMessage(payload, this.targetOrigin);
                            22 });
                            23 this.messageQueue = [];
                            24 }
                            25
                            26 destroy() {
                            27 this.iframe.remove();
                            28 this.isReady = false;
                            29 this.messageQueue = [];
                            30 }
                            31}
                            32
                            33const 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 }
                              6
                              7 async handlePaymentError(error, retryCallback) {
                              8 console.error("Payment error:", error);
                              9
                              10 if (this.currentRetries < this.maxRetries) {
                              11 this.currentRetries++;
                              12
                              13 // Show retry option to user
                              14 const shouldRetry = await showRetryDialog(
                              15 `Payment failed (${this.currentRetries}/${this.maxRetries}). Would you like to try again?`
                              16 );
                              17
                              18 if (shouldRetry) {
                              19 return retryCallback();
                              20 }
                              21 }
                              22
                              23 // Max retries reached or user declined retry
                              24 this.currentRetries = 0;
                              25 showFinalErrorMessage("Payment could not be completed. Please try a different payment method.");
                              26 }
                              27
                              28 reset() {
                              29 this.currentRetries = 0;
                              30 }
                              31}
                              32
                              33const 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 headers
                                2app.use((req, res, next) => {
                                3 // Prevent iframe embedding from untrusted domains
                                4 res.setHeader('X-Frame-Options', 'SAMEORIGIN');
                                5
                                6 // Content Security Policy
                                7 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());
                                14
                                15 // Prevent MIME type sniffing
                                16 res.setHeader('X-Content-Type-Options', 'nosniff');
                                17
                                18 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 validation
                                  2if (event.origin !== expectedOrigin) {
                                  3 console.warn(`Message rejected from origin: ${event.origin}`);
                                  4 return;
                                  5}
                                  6
                                  7// Check iframe loading
                                  8iframe.onload = () => {
                                  9 console.log("Iframe ready for postMessage communication");
                                  10};
                                  11
                                  12// Ensure proper sandbox attributes
                                  13const requiredSandboxAttrs = [
                                  14 'allow-scripts',
                                  15 'allow-same-origin',
                                  16 'allow-popups',
                                  17 'allow-forms'
                                  18];
                                  19
                                  20const 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.