Last updated: October 1, 2026
Use the 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:
clientId authentication (the standard path for saving to the vault)To save PayPal accounts with a one-time purchase:
The following concepts are central to the vault-with-purchase flow.
clientIdThe 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.
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.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.
The following steps describe the end-to-end vault-with-purchase flow.
onPayPalWebSdkLoaded().createInstance({ clientId }).session.start().POST /api/orders with paymentMethod: "paypal".store_in_vault: "ON_SUCCESS" in the PayPal payment source.onApprove fires, and the client calls POST /api/orders/:id/capture.GET /api/orders/:id and retrieves the vault token.To set up the front end:
clientIdAdd the SDK script tag to your page. The onload callback fires when the script is ready.
<script
async
src="https://www.sandbox.paypal.com/web-sdk/v6/core"
onload="onPayPalWebSdkLoaded()">
</script>Place the custom element where you want the button to render.
<paypal-button id="paypal-button" type="pay"></paypal-button>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 configurePayPalButton(sdkInstance);
}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 show the payment interface based on the buyer's device and browser.
async function configurePayPalButton(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 canceled:", 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")
);
});
}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 };
}Your server handles authentication, order creation with vault attributes, order capture, and vault token retrieval. The following topics explain how to configure these behaviors.
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=3000Set 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}`);
});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;
}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 });
});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());
});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 });
});Use this section to diagnose errors in the vault-with-purchase flow.
onPayPalWebSdkLoaded doesn't fireonload attribute, or the script URL points to the wrong environment, for example, sandbox instead of production.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 errorclientId doesn't match the app, the app doesn't have Save payment methods enabled, or the components array doesn't include "paypal-payments".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.session.start(), but the user has not selected a payment method. Browsers block popups that don't originate from a user action.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 tokenstore_in_vault: "ON_SUCCESS" in the payment source, or the capture failed before vaulting could complete.attributes.vault block. Check the capture response for errors before calling GET /api/orders/:id. The vault token appears after a successful capture.PAYPAL_CLIENT_ID and PAYPAL_CLIENT_SECRET match the app in the Developer Dashboard. Confirm the code uses api-m.sandbox.paypal.com for testing and api-m.paypal.com for production.