On this page
No Headings
Last updated: July 9, 2026
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:
Before implementing vaulted payments, obtain your credentials and enable vaulting features in the PayPal Developer Dashboard.
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_secretSetup token vs payment token:
Payment flows:
Usage patterns:
The PayPal save payment integration follows a specific flow:
Build an HTML page and a JavaScript file to set up your front end.
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 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;
}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;
}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 };
}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.
Step 1: Install the package.
npm install @paypal/paypal-server-sdkStep 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);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 a server-side endpoint that generates a setup token to start the customer-initiated vault flow.
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;
}
}
}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();
}The following code sample demonstrates how to configure a 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 };
}For additional support and questions: