Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.paypal.ai/llms.txt

Use this file to discover all available pages before exploring further.

This guide demonstrates how to integrate PayPal save payment functionality using the JavaScript SDK v6. Customers can securely save their PayPal payment methods for future transactions without making an immediate purchase. A common use case is offering a free trial and charging customers after the trial period ends. 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.
  • Maintain PCI compliance by leveraging PayPal’s secure vault system.

Prerequisites

Before implementing vaulted payments, obtain your credentials and enable vaulting features in the PayPal Developer Dashboard.
  1. Log in to the PayPal Developer Dashboard.
  2. In the navigation menu, use the Sandbox | Live toggle to select Live.
  3. Under Apps & Credentials > REST API apps, select your app name.
  4. Under API credentials, obtain the client ID and secret.
  5. To enable vaulting:
    • Go to Features > Payment capabilities.
    • Toggle on Save payment methods.
  6. To enable PayPal and Venmo vaulting:
    • Go to Features > Payment methods.
    • Toggle on PayPal and Venmo.

Set up your environment variables

In your project root, create a .env file and add your PayPal credentials:
PAYPAL_SANDBOX_CLIENT_ID=your_client_id
PAYPAL_SANDBOX_CLIENT_SECRET=your_client_secret
PayPal requires Risk Data Acquisition (RDA) to reduce fraud. You must implement risk data collection for all customer-initiated transactions (CIT) that use PayPal and Venmo Payment Tokens. Payment attempts that are missing RDA data are likely to be declined due to insufficient risk identifiers. Use the PayPal FraudNet and Magnes libraries to collect and pass RDA data during payment processing.

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 (not covered in this doc)
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 your browser-safe client token.
  2. Check eligibility for save payment functionality.
  3. Create save payment session using vault-specific session options.
  4. Create setup token on your server.
  5. Start save payment session to collect payment method.
  6. 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

Build an HTML page that loads the PayPal JavaScript SDK and renders the save payment button.
<!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

Initialize the SDK using a browser-safe client token, check eligibility for the vault flow, and render the save payment button if PayPal is eligible.
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
    });

    // Set up save payment button if eligible
    if (paymentMethods.isEligible("paypal")) {
      configurePayPalButton(sdkInstance);
    } else {
      console.log("PayPal save payment is not eligible for this session");
    }
  } catch (error) {
    console.error(error);
  }
}

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;
}

Configure the payment session

Define the session callbacks that handle approval, cancellation, and errors during the save payment flow.
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 and store payment token ID securely (server-side recommended)
      const createPaymentTokenResponse = await createPaymentToken(
        data.vaultSetupToken,
      );
      
      console.log("Payment token created:", createPaymentTokenResponse);
      
      // Handle successful save payment: show success message, etc.
    } catch (error) {
      console.error("Payment token creation failed:", error);
      // Show user-friendly error messages, optionally provide retry mechanism, etc.
    }
  },
  
  // Called when customer cancels the save payment flow
  onCancel(data) {
    console.log("Save payment cancelled:", data);
    // Show cancellation message, return user to appropriate flow, etc.
  },
  
  // Called when an error occurs during save payment
  onError(error) {
    console.error("Save payment error:", error);
  },
};

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 button to save payments

Create the save payment session and attach a click handler that starts the vault flow when the customer selects the PayPal button.
async function configurePayPalButton(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 {
      // Get the promise reference by invoking createVaultSetupToken()
      // Do not await this async function - it can cause transient activation issues
      // Read about transient activation at https://developer.mozilla.org/en-US/docs/Glossary/Transient_activation
      const createVaultSetupTokenPromise = createVaultSetupToken();

      // Start the save payment session
      await paypalPaymentSession.start(
        { 
          presentationMode: "auto" // Auto-detects best presentation mode
        },
        createVaultSetupTokenPromise
      );
    } catch (error) {
      console.error("Save payment start error:", error);
    }
  });
}

async function createVaultSetupToken() {
  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 };
}

Set up your backend

PayPal APIs can either be called directly, or by using the PayPal TypeScript Server SDK. The following examples use the @paypal/paypal-server-sdk npm package.

Set up the PayPal TypeScript Server SDK

Step 1: Install the package.
npm install @paypal/paypal-server-sdk
Step 2: Create and initialize the client and controllers using the client ID and secret you set in your environment variables.
import {
  ApiError,
  Client,
  CustomError,
  Environment,
  OAuthAuthorizationController,
  PaypalPaymentTokenUsageType,
  VaultController,
  VaultInstructionAction,
  VaultTokenRequestType
} from '@paypal/paypal-server-sdk';

import type {
  OAuthProviderError,
  PaymentTokenResponse,
  SetupTokenRequest
} from "@paypal/paypal-server-sdk";

const client = new Client({
  clientCredentialsAuthCredentials: {
    oAuthClientId: PAYPAL_SANDBOX_CLIENT_ID,
    oAuthClientSecret: PAYPAL_SANDBOX_CLIENT_SECRET
  },
  environment: Environment.Sandbox, // Use Environment.Production for live
});

const oAuthAuthorizationController = new OAuthAuthorizationController(client);
const vaultController = new VaultController(client);

Client token endpoint

Create a server-side endpoint that generates a browser-safe client token for authenticating the front end with the PayPal SDK.
app.get(
  "/paypal-api/auth/browser-safe-client-token",
  async (_req: Request, res: Response) => {
    try {
      const { jsonResponse, httpStatusCode } =
        await getBrowserSafeClientToken();
      res.status(httpStatusCode).json(jsonResponse);
    } catch (error) {
      console.error("Failed to create browser safe access token:", error);
      res
        .status(500)
        .json({ error: "Failed to create browser safe access token." });
    }
  },
);

export async function getBrowserSafeClientToken() {
  try {
    const auth = Buffer.from(
      `${PAYPAL_SANDBOX_CLIENT_ID}:${PAYPAL_SANDBOX_CLIENT_SECRET}`,
    ).toString("base64");

    const fieldParameters = {
      response_type: "client_token",
    };

    const { result, statusCode } =
      await oAuthAuthorizationController.requestToken(
        {
          authorization: `Basic ${auth}`,
        },
        fieldParameters,
      );

    // this interface is specific to the "client_token" response type
    interface ClientToken {
      accessToken: string;
      expiresIn: number;
      scope: string;
      tokenType: string;
    }

    const { accessToken, expiresIn, scope, tokenType } = result;
    const transformedResult: ClientToken = {
      accessToken,
      // expiresIn is a BigInt and must be converted to a Number
      expiresIn: Number(expiresIn),
      scope: String(scope),
      tokenType,
    };

    return {
      jsonResponse: transformedResult,
      httpStatusCode: statusCode,
    };
  } catch (error) {
    if (error instanceof ApiError) {
      const { result, statusCode } = error;
      type OAuthError = {
        error: OAuthProviderError;
        error_description?: string;
      };
      return {
        jsonResponse: result as OAuthError,
        httpStatusCode: statusCode,
      };
    } else {
      throw error;
    }
  }
}

Create setup token endpoint

Create a server-side endpoint that generates a setup token to start the customer-initiated vault flow.
PayPal requires Risk Data Acquisition (RDA) to reduce fraud. You must implement risk data collection for all customer-initiated transactions (CIT) that use PayPal and Venmo Payment Tokens. Payment attempts that are missing RDA data are likely to be declined due to insufficient risk identifiers. Use the PayPal FraudNet and Magnes libraries to collect and pass RDA data during payment processing.
app.post(
  "/paypal-api/vault/setup-token/create",
  async (_req: Request, res: Response) => {
    try {
      const { jsonResponse, httpStatusCode } =
        await createSetupTokenWithSampleDataForPayPal();
      res.status(httpStatusCode).json(jsonResponse);
    } catch (error) {
      console.error("Failed to create setup token:", error);
      res.status(500).json({ error: "Failed to create setup token." });
    }
  },
);

export async function createSetupTokenWithSampleDataForPayPal() {
  const defaultSetupTokenRequestBody = {
    paymentSource: {
      paypal: {
        experienceContext: {
          cancelUrl: "https://example.com/cancelUrl",
          returnUrl: "https://example.com/returnUrl",
          vaultInstruction: VaultInstructionAction.OnPayerApproval,
        },
        usageType: PaypalPaymentTokenUsageType.Merchant,
      },
    },
  };

  return createSetupToken(defaultSetupTokenRequestBody, Date.now().toString());
}

export async function createSetupToken(
  setupTokenRequestBody: SetupTokenRequest,
  paypalRequestId?: string,
) {
  try {
    const { result, statusCode } = await vaultController.createSetupToken({
      body: setupTokenRequestBody,
      paypalRequestId,
    });

    return {
      jsonResponse: result,
      httpStatusCode: statusCode,
    };
  } catch (error) {
    if (error instanceof ApiError) {
      const { result, statusCode } = error;

      return {
        jsonResponse: result as CustomError,
        httpStatusCode: statusCode,
      };
    } else {
      throw error;
    }
  }
}

Create payment token endpoint

Payment tokens are long-lived values for making future payments. PayPal recommends storing them in your database. Do not pass them to the browser.
app.post(
  "/paypal-api/vault/payment-token/create",
  async (req: Request, res: Response) => {
    try {
      const { jsonResponse, httpStatusCode } = await createPaymentToken(
        req.body.vaultSetupToken as string,
      );

      const paymentTokenResponse = jsonResponse as PaymentTokenResponse;

      if (paymentTokenResponse.id) {
        // This payment token id is a long-lived value for making future payments.
        // PayPal recommends storing this value in your database
        // and NOT returning it back to the browser.
        await savePaymentTokenToDatabase(paymentTokenResponse);
        res.status(httpStatusCode).json({
          status: "SUCCESS",
          description:
            "Payment token saved to database for future transactions",
        });
      } else {
        res.status(httpStatusCode).json({
          status: "ERROR",
          description: "Failed to create payment token",
        });
      }
    } catch (error) {
      console.error("Failed to create payment token:", error);
      res.status(500).json({ error: "Failed to create payment token." });
    }
  },
);

export async function createPaymentToken(
  vaultSetupToken: string,
  paypalRequestId?: string,
) {
  try {
    const { result, statusCode } = await vaultController.createPaymentToken({
      paypalRequestId: paypalRequestId ?? Date.now().toString(),
      body: {
        paymentSource: {
          token: {
            id: vaultSetupToken,
            type: VaultTokenRequestType.SetupToken,
          },
        },
      },
    });

    return {
      jsonResponse: result,
      httpStatusCode: statusCode,
    };
  } catch (error) {
    if (error instanceof ApiError) {
      const { result, statusCode } = error;

      return {
        jsonResponse: result as CustomError,
        httpStatusCode: statusCode,
      };
    } else {
      throw error;
    }
  }
}

async function savePaymentTokenToDatabase(
  paymentTokenResponse: PaymentTokenResponse,
) {
  // example function to teach saving the paymentToken to a database
  // to be used for future transactions
  return Promise.resolve();
}

Advanced features

The following code sample demonstrates how to configure a custom setup token.

Configure custom setup token

Use a custom setup token to pass additional payer and merchant configuration to the vault flow.
async function createCustomSetupToken(customerInfo = {}) {
  const customSetupTokenRequestBody = {
    payment_source: {
      paypal: {
        description: customerInfo.description || "Save PayPal payment method",
        usagePattern: customerInfo.usagePattern || "IMMEDIATE",
        usageType: customerInfo.usageType || "MERCHANT",
        customerType: customerInfo.customerType || "CONSUMER",
        permitMultiplePaymentTokens: 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",
    },
    // set up endpoint to parse request body
    body: JSON.stringify(customSetupTokenRequestBody),
  });

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

Resources

Support

For additional support and questions: