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.

Use the PayPal JavaScript SDK v6 to save a buyer’s PayPal information when they complete a one-time payment. PayPal stores the buyer’s information in a secure data store called a vault. When a returning buyer selects PayPal at checkout, they can pay with their saved account without re-entering credentials or payment details. With this integration, you can:
  • Collect and securely store a buyer’s PayPal account at checkout
  • Retrieve a vault token after a successful capture
  • Use clientId authentication (the standard path for saving to the vault)

Prerequisites

To save PayPal accounts with a one-time purchase, you need:
  • Complete the steps in get started to get:
    • An app in the PayPal Developer Dashboard with Save payment methods enabled
    • A client ID
    • A client secret
  • A server-side environment that can make HTTPS requests to the PayPal API

Key concepts

The following concepts are central to the vault-with-purchase flow.

Use clientId

The PayPal JavaScript SDK v6 initializes with your clientId. Your clientId is safe to expose in the browser and is the standard authentication path for PayPal one-time payments, vaulting, and return-buyer flows. Only Fastlane integrations require the alternate clientToken authentication path.

Vault attributes

Use the following vault attributes to support saving PayPal accounts with a purchase:
  • store_in_vault: "ON_SUCCESS" saves the payment method when the order is successfully captured.
  • usage_type: "MERCHANT" indicates that the merchant manages the vault.
  • customer_type: "CONSUMER" identifies the buyer as a consumer to present a consumer vaulting experience.

Vault token

After a successful capture, retrieve the vault token ID by calling GET /v2/checkout/orders/:id. Link the token at payment_source.paypal.attributes.vault.id to the customer ID and store it on your server side for future use.

Integration flow

The following steps describe the end-to-end vault-with-purchase flow.
  1. The page loads, and the SDK script fires onPayPalWebSdkLoaded().
  2. The SDK initializes with createInstance({ clientId }).
  3. The buyer selects the PayPal button, which calls session.start().
  4. The client calls POST /api/orders with paymentMethod: "paypal".
  5. The server creates the order with store_in_vault: "ON_SUCCESS" in the PayPal payment source.
  6. The buyer authenticates in the PayPal window.
  7. onApprove fires, and the client calls POST /api/orders/:id/capture.
  8. The client calls GET /api/orders/:id and retrieves the vault token.

Set up your front end

To set up the front end:

Load the SDK

Add the SDK script tag to your page. The onload callback fires when the script is ready.
<!-- Sandbox -->
<script
  async
  src="https://www.sandbox.paypal.com/web-sdk/v6/core"
  onload="onPayPalWebSdkLoaded()">
</script>
<!-- Production -->
<script
  async
  src="https://www.paypal.com/web-sdk/v6/core"
  onload="onPayPalWebSdkLoaded()">
</script>

Add the button element to the page

Place the custom element where you want the button to render:
<paypal-button id="paypal-button" type="pay"></paypal-button>

Initialize the SDK

After the SDK loads, use your clientId to create an instance and register the PayPal payments component.
async function onPayPalWebSdkLoaded() {
  const sdkInstance = await window.paypal.createInstance({
    clientId: "YOUR_CLIENT_ID",
    components: ["paypal-payments"],
    pageType: "checkout",
  });

  await setupPayPalButton(sdkInstance);
}

Create a payment session

Set up a one-time payment session that includes vault support. The onApprove callback captures the order and retrieves the vault token. The SDK uses presentationMode: "auto" to determine whether to use a popup or redirect to display the payment interface based on the buyer’s device and browser.
async function setupPayPalButton(sdkInstance) {
  const session = sdkInstance.createPayPalOneTimePaymentSession({
    onApprove: async ({ orderId }) => {
      // Capture the approved order
      await fetch(`/api/orders/${orderId}/capture`, { method: "POST" });

      // GET the order to surface the vault token
      const orderRes = await fetch(`/api/orders/${orderId}`);
      const { response: order } = await orderRes.json();

      const vaultToken = order?.payment_source?.paypal?.attributes?.vault?.id;
      console.log("Vault token:", vaultToken);
      // Store vaultToken server-side linked to the customer ID
    },
    onCancel: ({ orderId }) => {
      console.log("Buyer cancelled:", orderId);
    },
    onError: (err) => {
      console.error("PayPal error:", err.code, err.message);
    },
  });

  document.getElementById("paypal-button").addEventListener("click", async () => {
    await session.start(
      { presentationMode: "auto" },
      createOrder("paypal")
    );
  });
}

Create an order (client)

This helper calls your server to create an order and returns the orderId for the SDK to start the checkout flow.
async function createOrder(paymentMethod) {
  const res = await fetch("/api/orders", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ paymentMethod }),
  });
  const data = await res.json();
  if (!data.id) throw new Error("Order creation failed");
  return { orderId: data.id };
}

Set up your back end

Your server handles authentication, order creation with vault attributes, order capture, and vault token retrieval.

Environment variables

Paste the following variables in an environment (.env) file in your project root. Replace your_client_id and your_client_secret with your app’s client ID and client secret.
PAYPAL_CLIENT_ID=your_client_id
PAYPAL_CLIENT_SECRET=your_client_secret
PORT=3000

Server setup

Set up an Express server with JSON parsing and static file serving.
const express = require("express");
const path = require("path");
require("dotenv").config();

const app = express();
app.use(express.json());
app.use(express.static(path.join(__dirname, "public")));

// ... route handlers below ...

app.listen(process.env.PORT || 3000, () => {
  console.log(`Server running on port ${process.env.PORT || 3000}`);
});

Get an access token

The order and capture endpoints need a server-side access token. This helper fetches one from the OAuth endpoint:
async function getAccessToken() {
  const credentials = Buffer.from(
    `${process.env.PAYPAL_CLIENT_ID}:${process.env.PAYPAL_CLIENT_SECRET}`
  ).toString("base64");

  const tokenRes = await fetch("https://api-m.sandbox.paypal.com/v1/oauth2/token", {
    method: "POST",
    headers: {
      Authorization: `Basic ${credentials}`,
      "Content-Type": "application/x-www-form-urlencoded",
    },
    body: "grant_type=client_credentials",
  });

  const { access_token } = await tokenRes.json();
  return access_token;
}

Create an order with vault attributes

This endpoint creates an order with store_in_vault: "ON_SUCCESS" in the PayPal payment source, which tells the system to save the buyer’s payment method to the vault after a successful capture.
app.post("/api/orders", async (req, res) => {
  const { paymentMethod } = req.body;
  const origin = req.headers.origin ?? `http://${req.headers.host}`;
  const accessToken = await getAccessToken();

  const orderRes = await fetch("https://api-m.sandbox.paypal.com/v2/checkout/orders", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${accessToken}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      intent: "CAPTURE",
      payment_source: {
        paypal: {
          experience_context: {
            return_url: `${origin}/`,
            cancel_url: `${origin}/`,
            shipping_preference: "GET_FROM_FILE",
            user_action: "CONTINUE",
          },
          attributes: {
            vault: {
              store_in_vault: "ON_SUCCESS",
              usage_type: "MERCHANT",
              customer_type: "CONSUMER",
            },
          },
        },
      },
      purchase_units: [{
        amount: {
          currency_code: "USD",
          value: "100.00",
        },
      }],
    }),
  });

  const order = await orderRes.json();
  res.json({ id: order.id });
});

Capture the order

After the buyer approves payment, call the Orders API to capture the funds.
app.post("/api/orders/:orderID/capture", async (req, res) => {
  const accessToken = await getAccessToken();

  const captureRes = await fetch(
    `https://api-m.sandbox.paypal.com/v2/checkout/orders/${req.params.orderID}/capture`,
    {
      method: "POST",
      headers: {
        Authorization: `Bearer ${accessToken}`,
        "Content-Type": "application/json",
      },
    }
  );

  res.json(await captureRes.json());
});

Retrieve the vault token

Fetch the completed order to extract the vault token from the response. The token is at payment_source.paypal.attributes.vault.id. When you have the token, store it in your database, linked to the customer’s account. After you do that, returning customers can check out without re-entering payment details.
app.get("/api/orders/:orderID", async (req, res) => {
  const accessToken = await getAccessToken();

  const orderRes = await fetch(
    `https://api-m.sandbox.paypal.com/v2/checkout/orders/${req.params.orderID}`,
    { headers: { Authorization: `Bearer ${accessToken}` } }
  );

  const order = await orderRes.json();
  // Vault token at: order.payment_source.paypal.attributes.vault.id
  res.json({ response: order });
});

Troubleshooting

Use this section to diagnose errors in the vault-with-purchase flow.

onPayPalWebSdkLoaded doesn’t fire

  • Cause: The script tag doesn’t include the onload attribute, or the script URL points to the wrong environment, for example, sandbox instead of production.
  • Fix: Add onload="onPayPalWebSdkLoaded()" to the script tag. Use sandbox.paypal.com for testing. Remove the sandbox. subdomain for the production environment.

createInstance throws or returns an error

  • Cause: The clientId doesn’t match the app, the app doesn’t have Save payment methods enabled, or the components array doesn’t include "paypal-payments".
  • Fix: Verify that the clientId matches the app in the PayPal Developer Dashboard. Verify the toggle is set to Save payment methods on under the app’s features. Include components: ["paypal-payments"] in the createInstance call.

The popup is blocked

  • Cause: Your code calls session.start(), but the user has not selected a payment method. Browsers block popups that don’t originate from a user action.
  • Fix: Call session.start() inside the button’s click event listener without any await calls before it. Move async work, such as order creation, into the createOrder callback that session.start() accepts as its second argument.

onApprove fires with missing vault token

  • Cause: Your server created the order without store_in_vault: "ON_SUCCESS" in the payment source, or the capture failed before vaulting could complete.
  • Fix: Confirm that the server-side order body includes the full attributes.vault block. Check the capture response for errors before calling GET /api/orders/:id. The vault token appears after a successful capture.

Order creation returns a 401 or 403

  • Cause: The server-side access token expired, the credentials are wrong, or your server fetched the token from the wrong environment endpoint.
  • Fix: Confirm PAYPAL_CLIENT_ID and PAYPAL_CLIENT_SECRET match the app in the Developer Dashboard. For testing, fetch the token from api-m.sandbox.paypal.com. Remove the sandbox. subdomain for the production environment.