Skip to main content
This guide demonstrates how to integrate PayPal save payment functionality using the JavaScript SDK v6, allowing customers to securely save their PayPal payment methods for future transactions without making an immediate purchase. The PayPal Save Payment integration enables you to:
  • Collect and securely store PayPal payment methods without charging the customer
  • Create payment tokens for future use in subsequent transactions
  • Provide a streamlined checkout experience for returning customers
  • Maintain PCI compliance by leveraging PayPal’s secure vault system

Prerequisites

Set up your PayPal account:
  • Create a PayPal developer, personal, or business account
  • Visit the PayPal Developer Dashboard
  • Create a sandbox application to obtain Client ID and Secret
  • Ensure your application has Vault permissions enabled
Enable vaulting:
  1. Log in to the Developer Dashboard.
  2. Under REST API apps, select your app name.
  3. Under Sandbox App Settings > App Feature Options, check Accept payments.
  4. Expand Advanced options. Confirm that Vault is selected.
Configure your environment: In your root folder, create an .env file with your PayPal credentials:
PAYPAL_SANDBOX_CLIENT_ID=your_client_id
PAYPAL_SANDBOX_CLIENT_SECRET=your_client_secret

Key concepts

Setup token vs payment token:
  • Setup Token: Temporary token used during the save payment flow
  • Payment Token: Permanent token stored in PayPal’s vault for future use
  • Conversion: Setup tokens are converted to payment tokens after customer approval
Payment flows:
  • VAULT_WITHOUT_PAYMENT: Save payment method without making a purchase
  • VAULT_WITH_PAYMENT: Save payment method while making a purchase
Usage patterns:
  • IMMEDIATE: Token will be used right away
  • DEFERRED: Token will be used at a future date

Integration flow

The PayPal save payment integration follows a specific flow:
  1. Initialize PayPal SDK with vault-specific configuration
  2. Check eligibility for save payment functionality
  3. Create setup token on your server
  4. Start save payment session to collect payment method
  5. Create payment token from vault setup token for future use

Set up your front end

Build an HTML page and a JavaScript file to set up your front end.

Build an HTML page

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Save Payment - PayPal JavaScript SDK</title>
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <style>
      .buttons-container {
        display: flex;
        flex-direction: column;
        gap: 12px;
      }
    </style>
  </head>
  <body>
    <h1>Save Payment Integration</h1>

    <div class="buttons-container">
      <!-- PayPal save payment button, initially hidden -->
      <paypal-button id="paypal-button" hidden></paypal-button>
    </div>
    
    <script src="app.js"></script>

    <!-- Load PayPal SDK -->
    <script
      async
      src="https://www.sandbox.paypal.com/web-sdk/v6/core"
      onload="onPayPalWebSdkLoaded()"
    ></script>
  </body>
</html>

Initialize the SDK to save payment methods

async function onPayPalWebSdkLoaded() {
  try {
    // Get client token for authentication
    const clientToken = await getBrowserSafeClientToken();
    
    // Create PayPal SDK instance
    const sdkInstance = await window.paypal.createInstance({
      clientToken,
      components: ["paypal-payments"],
      pageType: "checkout",
    });

    // Check eligibility for save payment with vault flow
    const paymentMethods = await sdkInstance.findEligibleMethods({
      currencyCode: "USD",
      paymentFlow: "VAULT_WITHOUT_PAYMENT", // Specify vault flow without payment
    });

    // Setup save payment button if eligible
    if (paymentMethods.isEligible("paypal")) {
      setupPayPalButton(sdkInstance);
    } else {
      console.log("PayPal save payment is not eligible for this session");
      showNotEligibleMessage();
    }
  } catch (error) {
    console.error("SDK initialization error:", error);
    handleInitializationError(error);
  }
}

Configure the payment session

const paymentSessionOptions = {
  // Called when customer approves saving their payment method
  async onApprove(data) {
    console.log("Save payment approved:", data);
    
    try {
      // Create payment token from vault setup token
      const createPaymentTokenResponse = await createPaymentToken(
        data.vaultSetupToken,
      );
      
      console.log("Payment token created:", createPaymentTokenResponse);
      
      // Handle successful save payment
      handleSavePaymentSuccess(createPaymentTokenResponse);
      
    } catch (error) {
      console.error("Payment token creation failed:", error);
      handleSavePaymentError(error);
    }
  },
  
  // Called when customer cancels the save payment flow
  onCancel(data) {
    console.log("Save payment cancelled:", data);
    handleSavePaymentCancellation();
  },
  
  // Called when an error occurs during save payment
  onError(error) {
    console.error("Save payment error:", error);
    handleSavePaymentError(error);
  },
};

Set up button to save payments

async function setupPayPalButton(sdkInstance) {
  // Create PayPal save payment session
  const paypalPaymentSession = sdkInstance.createPayPalSavePaymentSession(
    paymentSessionOptions,
  );

  // Get reference to PayPal button
  const paypalButton = document.querySelector("#paypal-button");
  paypalButton.removeAttribute("hidden");

  // Add click handler to start save payment flow
  paypalButton.addEventListener("click", async () => {
    try {
      // Start the save payment session
      await paypalPaymentSession.start(
        { 
          presentationMode: "auto" // Auto-detects best presentation mode
        },
        createSetupToken() // Create setup token for vault
      );
    } catch (error) {
      console.error("Save payment start error:", error);
      handleSavePaymentError(error);
    }
  });
}

Set up your backend

The save payment integration requires these server-side endpoints:

Client Token endpoint

// GET /paypal-api/auth/browser-safe-client-token
async function getBrowserSafeClientToken() {
  const response = await fetch("/paypal-api/auth/browser-safe-client-token", {
    method: "GET",
    headers: {
      "Content-Type": "application/json",
    },
  });
  const { accessToken } = await response.json();
  return accessToken;
}

Create Setup Token endpoint

// POST /paypal-api/vault/setup-token/create
async function createSetupToken() {
  const response = await fetch("/paypal-api/vault/setup-token/create", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
  });
  const { id } = await response.json();
  
  return { setupToken: id };
}

Create Payment Token endpoint

// POST /paypal-api/vault/payment-token/create
async function createPaymentToken(vaultSetupToken) {
  const response = await fetch("/paypal-api/vault/payment-token/create", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ vaultSetupToken }),
  });
  const data = await response.json();
  
  return data;
}

Set up token creation on your server

On your server, create a setup token for vault operations:
// Example Node.js server endpoint
app.post('/paypal-api/vault/setup-token/create', async (req, res) => {
  try {
    const setupTokenPayload = {
      payment_source: {
        paypal: {
          description: "Save PayPal payment method",
          usage_pattern: "IMMEDIATE", // or "DEFERRED"
          usage_type: "MERCHANT", // or "PLATFORM"
          customer_type: "CONSUMER", // or "BUSINESS"
          permit_multiple_payment_tokens: false,
        }
      }
    };

    const setupTokenResponse = await paypalClient.vaultSetupTokensCreate({
      body: setupTokenPayload
    });

    res.json({ 
      id: setupTokenResponse.result.id 
    });
  } catch (error) {
    console.error('Setup token creation error:', error);
    res.status(500).json({ error: 'Failed to create setup token' });
  }
});

Set up Payment Token creation on your server

Convert the vault setup token to a reusable payment token:
// Example Node.js server endpoint
app.post('/paypal-api/vault/payment-token/create', async (req, res) => {
  try {
    const { vaultSetupToken } = req.body;

    const paymentTokenPayload = {
      payment_source: {
        token: {
          id: vaultSetupToken,
          type: "SETUP_TOKEN"
        }
      }
    };

    const paymentTokenResponse = await paypalClient.vaultPaymentTokensCreate({
      body: paymentTokenPayload
    });

    res.json({
      id: paymentTokenResponse.result.id,
      status: paymentTokenResponse.result.status,
      payment_source: paymentTokenResponse.result.payment_source
    });
  } catch (error) {
    console.error('Payment token creation error:', error);
    res.status(500).json({ error: 'Failed to create payment token' });
  }
});

Use saved payment tokens

Once you have a payment token, you can use it for future transactions:

Create order with saved payment methods

async function createOrderWithSavedPayment(paymentTokenId, orderAmount) {
  const orderPayload = {
    intent: "CAPTURE",
    purchase_units: [{
      amount: {
        currency_code: "USD",
        value: orderAmount
      }
    }],
    payment_source: {
      paypal: {
        vault_id: paymentTokenId, // Use the saved payment token
        stored_in_vault: "ON_SUCCESS"
      }
    }
  };

  const response = await fetch("/paypal-api/checkout/orders/create", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify(orderPayload),
  });

  const { id } = await response.json();
  return { orderId: id };
}

Create a server-side order with a vault ID

// Example server endpoint for orders using saved payment methods
app.post('/paypal-api/checkout/orders/create-with-vault', async (req, res) => {
  try {
    const { vaultId, amount } = req.body;

    const orderPayload = {
      intent: "CAPTURE",
      purchase_units: [{
        amount: {
          currency_code: "USD",
          value: amount
        }
      }],
      payment_source: {
        paypal: {
          vault_id: vaultId,
          stored_in_vault: "ON_SUCCESS"
        }
      }
    };

    const orderResponse = await paypalClient.ordersCreate({
      body: orderPayload
    });

    res.json({ 
      id: orderResponse.result.id 
    });
  } catch (error) {
    console.error('Order creation error:', error);
    res.status(500).json({ error: 'Failed to create order' });
  }
});

Advanced Features

The following code samples demonstrate how to configure a custom setup token and manage payment tokens.

Configure custom setup token

async function createCustomSetupToken(customerInfo = {}) {
  const setupTokenPayload = {
    payment_source: {
      paypal: {
        description: customerInfo.description || "Save PayPal payment method",
        usage_pattern: customerInfo.usagePattern || "IMMEDIATE",
        usage_type: customerInfo.usageType || "MERCHANT",
        customer_type: customerInfo.customerType || "CONSUMER",
        permit_multiple_payment_tokens: customerInfo.allowMultiple || false,
      }
    },
    // Optional: Add customer information
    ...(customerInfo.customerId && {
      customer: {
        id: customerInfo.customerId
      }
    })
  };

  const response = await fetch("/paypal-api/vault/setup-token/create", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify(setupTokenPayload),
  });

  const { id } = await response.json();
  return { setupToken: id };
}

Manage payment token

// Get payment token details
async function getPaymentTokenDetails(paymentTokenId) {
  const response = await fetch(`/paypal-api/vault/payment-token/${paymentTokenId}`, {
    method: "GET",
    headers: {
      "Content-Type": "application/json",
    },
  });
  
  return await response.json();
}

// Delete payment token
async function deletePaymentToken(paymentTokenId) {
  const response = await fetch(`/paypal-api/vault/payment-token/${paymentTokenId}`, {
    method: "DELETE",
    headers: {
      "Content-Type": "application/json",
    },
  });
  
  return response.ok;
}

// List customer payment tokens
async function listCustomerPaymentTokens(customerId) {
  const response = await fetch(`/paypal-api/vault/payment-tokens?customer_id=${customerId}`, {
    method: "GET",
    headers: {
      "Content-Type": "application/json",
    },
  });
  
  return await response.json();
}

Error handling

function handleSavePaymentError(error) {
  console.error("Save payment error details:", error);
  
  // Show user-friendly error messages based on error type
  const errorMessage = getSavePaymentErrorMessage(error);
  showErrorToUser(errorMessage);
  
  // Optionally provide retry mechanism
  showRetryOption();
}

function getSavePaymentErrorMessage(error) {
  switch (error.code) {
    case 'VAULT_NOT_ENABLED':
      return 'Payment saving is not available. Please contact support.';
    case 'CUSTOMER_NOT_ELIGIBLE':
      return 'Your account is not eligible for saving payment methods.';
    case 'SETUP_TOKEN_EXPIRED':
      return 'Setup session expired. Please try again.';
    case 'NETWORK_ERROR':
      return 'Network error occurred. Please check your connection and try again.';
    default:
      return 'Unable to save payment method. Please try again.';
  }
}

function handleSavePaymentSuccess(paymentTokenResponse) {
  console.log("Payment method saved successfully:", paymentTokenResponse);
  
  // Store payment token ID securely (server-side recommended)
  const paymentTokenId = paymentTokenResponse.id;
  savePaymentTokenToDatabase(paymentTokenId);
  
  // Show success message
  showSuccessMessage('Payment method saved successfully!');
  
  // Update UI to reflect saved payment method
  updatePaymentMethodsUI(paymentTokenResponse);
  
  // Track conversion for analytics
  trackSavePaymentSuccess(paymentTokenResponse);
}

function handleSavePaymentCancellation() {
  console.log("Save payment cancelled by user");
  
  // Show cancellation message
  showInfoMessage('Payment method saving was cancelled.');
  
  // Return user to appropriate flow
  returnToPaymentOptions();
}

Security best practices

Follow best practices for client and server-side security.

Client-side security

// Never store payment tokens in client-side storage
// Always validate on server-side
function savePaymentTokenSecurely(paymentToken) {
  // ❌ NEVER do this
  // localStorage.setItem('paymentToken', paymentToken.id);
  
  // ✅ Always send to server for secure storage
  return fetch('/api/customer/payment-methods', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${userToken}`, // Use proper authentication
    },
    body: JSON.stringify({
      paymentTokenId: paymentToken.id,
      customerId: getCurrentCustomerId(),
    }),
  });
}

Server-side security

// Example secure payment token storage
async function storePaymentTokenSecurely(req, res) {
  try {
    // Validate user authentication
    const userId = validateUserToken(req.headers.authorization);
    if (!userId) {
      return res.status(401).json({ error: 'Unauthorized' });
    }

    const { paymentTokenId } = req.body;
    
    // Validate payment token with PayPal
    const tokenDetails = await validatePaymentToken(paymentTokenId);
    if (!tokenDetails.valid) {
      return res.status(400).json({ error: 'Invalid payment token' });
    }

    // Store securely in database with encryption
    await database.paymentTokens.create({
      user_id: userId,
      token_id: encrypt(paymentTokenId),
      token_details: encrypt(JSON.stringify(tokenDetails)),
      created_at: new Date(),
    });

    res.json({ success: true });
  } catch (error) {
    console.error('Token storage error:', error);
    res.status(500).json({ error: 'Failed to store payment method' });
  }
}

Integration patterns

Common integration patterns include checkout with a save payment option and customer payment method management.

Checkout with save payment option

function createCheckoutWithSaveOption() {
  return `
    <div class="checkout-options">
      <div class="payment-section">
        <h3>Payment Method</h3>
        
        <!-- Existing saved payment methods -->
        <div id="saved-payment-methods"></div>
        
        <!-- Option to use new payment method -->
        <div class="new-payment-method">
          <label>
            <input type="radio" name="payment-option" value="new">
            Use a new payment method
          </label>
          
          <div class="new-payment-controls" style="display: none;">
            <label>
              <input type="checkbox" id="save-payment-method">
              Save this payment method for future purchases
            </label>
            
            <div class="payment-buttons">
              <paypal-button id="paypal-button" hidden></paypal-button>
            </div>
          </div>
        </div>
      </div>
    </div>
  `;
}

Manage customer payment methods

async function loadCustomerPaymentMethods() {
  try {
    const response = await fetch('/api/customer/payment-methods', {
      headers: {
        'Authorization': `Bearer ${userToken}`,
      },
    });
    
    const paymentMethods = await response.json();
    renderPaymentMethods(paymentMethods);
  } catch (error) {
    console.error('Failed to load payment methods:', error);
  }
}

function renderPaymentMethods(paymentMethods) {
  const container = document.getElementById('saved-payment-methods');
  
  paymentMethods.forEach(method => {
    const methodElement = document.createElement('div');
    methodElement.className = 'saved-payment-method';
    methodElement.innerHTML = `
      <label>
        <input type="radio" name="payment-option" value="${method.id}">
        <span class="method-info">
          PayPal ending in ${method.last4 || '****'}
          <small>Expires ${method.expiry || 'N/A'}</small>
        </span>
      </label>
      <button onclick="deletePaymentMethod('${method.id}')" class="delete-btn">
        Remove
      </button>
    `;
    container.appendChild(methodElement);
  });
}

Test

Use PayPal sandbox accounts for testing:
  • Create test business and personal accounts
  • Test with different countries and currencies
  • Verify vault permissions are enabled
Test the following scenarios:
  1. Successful Save: Complete save payment flow successfully
  2. User Cancellation: Test cancellation handling
  3. Network Errors: Simulate network failures
  4. Invalid Tokens: Test with expired or invalid setup tokens
  5. Multiple Saves: Test saving multiple payment methods
  6. Token Usage: Test using saved tokens for payments

Production checklist

  • Replace sandbox URLs with production URLs
  • Update environment configuration for production
  • Verify vault permissions are enabled in production
  • Implement secure payment token storage
  • Add comprehensive error handling
  • Set up monitoring and alerting
  • Test payment token usage workflows
  • Implement token lifecycle management
  • Add customer payment method management UI
  • Verify PCI compliance requirements
  • Test across different browsers and devices
  • Implement proper authentication and authorization

Technical limitations

  • Setup tokens have limited lifetime (typically 3 hours)
  • Payment tokens are specific to the merchant account
  • Vault functionality requires special permissions
  • Not all payment methods support vaulting

Business considerations

  • Consider customer consent and privacy regulations
  • Implement proper data retention policies
  • Provide clear opt-in/opt-out mechanisms
  • Handle payment method updates and expiration

Resources

Support

For additional support and questions:
I