On this page
No Headings
Last updated: June 18, 2026
Save payment methods to charge payers after a set amount of time. For example, you can offer a free trial and charge payers after the trial expires. Payers don't need to be present when charged. No checkout required.
Use the JavaScript SDK to save a payer's card if you aren't PCI Compliant - SAQ A but want to save credit or debit cards during checkout.
createCardFieldsSavePaymentSession() method to save a card without a transaction.PayPal encrypts payment method information and stores it in a digital vault for that customer.
Set up your sandbox and live business accounts to save payment methods:
Pass your client ID to the SDK to identify yourself.
Replace CLIENT-ID with your app's client ID in the following sample:
<script async src="https://sandbox.paypal.com/web-sdk/v6/core" onload="onPayPalWebSdkLoaded()"></script>You request a setup token from your server. The setup token creation for v6 now supports optional 3D Secure verification per your SCA policy.
This example for v6 includes optional 3DS or SCA behavior:
curl -v -k -X POST 'https://api-m.sandbox.paypal.com/v3/vault/setup-tokens' \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ACCESS-TOKEN" \
-H "PayPal-Request-Id: REQUEST-ID" \
-d '{
"payment_source": {
"card": {
"verification_method": "SCA_WHEN_REQUIRED",
"experience_context": {
"return_url": "https://example.com/returnUrl",
"cancel_url": "https://example.com/cancelUrl"
}
}
}
}'Create a setup token for cards that have:
verification_method"SCA_ALWAYS" or "SCA_WHEN_REQUIRED"const sdk = await window.paypal.createInstance({ clientToken, components: ["card-fields"] });
const session = sdk.createCardFieldsSavePaymentSession();
// ... render number/cvv/expiry as usual
const { setupToken } = await createVaultSetupToken({ enable3DS, scaMethod });
const { state, data } = await session.submit(setupToken);
switch (state) {
case "succeeded": {
// Persist data.vaultSetupToken (or returned instrument id) on your server
break;
}
case "canceled": {
// Buyer dismissed challenge
break;
}
case "failed": {
// Show retry message, inspect data.message
break;
}
}Make this request from your server.
This setup token is generated with an empty card in the payment_source object. PayPal hosted fields use this token to securely update the setup token with card details.
Copy and modify the following code:
ACCESS-TOKEN to your sandbox app's access token.REQUEST-ID to a set of unique alphanumeric characters such as a time stamp.createVaultSetupToken, call the endpoint on your server to create a setup token with the Payment Method Tokens API. createVaultSetupToken returns the setup token as a string.curl -v -k -X POST 'https://api-m.sandbox.paypal.com/v3/vault/setup-tokens'
-H "Content-Type: application/json"
-H "Authorization: Bearer ACCESS-TOKEN"
-H "PayPal-Request-Id: REQUEST-ID"
-d '{
"payment_source": {
"card": {}
}
}'
`}When you want to vault a card without taking a payment, use the Save Payment Session on the client: sdk.createCardFieldsSavePaymentSession().
// Initialize the v6 SDK
const sdk = await window.paypal.createInstance({
clientToken,
components: ["card-fields"]
});
// Create the session
const session = sdk.createCardFieldsSavePaymentSession();
// Render the card fields (you still need this part)
session.render({
fields: {
number: { selector: '#card-number' },
cvv: { selector: '#card-cvv' },
expirationDate: { selector: '#card-expiry' }
}
});
// When user submits the form (replace the automatic callbacks)
async function handleSubmit() {
try {
// This replaces your createVaultSetupToken callback
const result = await fetch("example.com/create/setup/token");
const setupToken = result.token;
// Submit with the session (this is the new way)
const { state, data } = await session.submit(setupToken);
switch (state) {
case "succeeded": {
// This replaces your onApprove callback
await fetch("example.com/create/payment/token", {
body: JSON.stringify({ vaultSetupToken: data.vaultSetupToken })
});
break;
}
case "canceled": {
// Handle when user cancels
break;
}
case "failed": {
// Handle errors
console.error(data.message);
break;
}
}
} catch (error) {
console.error("Error:", error);
}
}// Initialize the v6 SDK
const sdk = await window.paypal.createInstance({
clientToken,
components: ["card-fields"]
});
// Create the session
const session = sdk.createCardFieldsSavePaymentSession();
// Render the card fields (you still need this part)
session.render({
fields: {
number: { selector: '#card-number' },
cvv: { selector: '#card-cvv' },
expirationDate: { selector: '#card-expiry' }
}
});
// When user submits the form (replace the automatic callbacks)
async function handleSubmit() {
try {
// This replaces your createVaultSetupToken callback
const result = await fetch("example.com/create/setup/token");
const setupToken = result.token;
// Submit with the session (this is the new way)
const { state, data } = await session.submit(setupToken);
switch (state) {
case "succeeded": {
// This replaces your onApprove callback
// Send the vaultSetupToken to your server for later use
const vaultSetupToken = data.vaultSetupToken;
// Do whatever you need with the token here
console.log("Vault setup token:", vaultSetupToken);
break;
}
case "canceled": {
// Handle when user cancels
break;
}
case "failed": {
// This replaces your onError callback
console.error('Something went wrong:', data.message);
break;
}
}
} catch (error) {
// This also replaces your onError callback for network/other errors
console.error('Something went wrong:', error);
}
}Make this request from your server. Copy and modify the following code.
vaultSetupToken returned by onApprove to your server.ACCESS-TOKEN to your sandbox app's access token.REQUEST-ID to a set of unique alphanumeric characters such as a time stamp.VAULT-SETUP-TOKEN to the value passed from the client.payment token returned from the API to use in future transactions.curl -v -k -X POST 'https://api-m.sandbox.paypal.com/v3/vault/payment-tokens'
-H "Content-Type: application/json"
-H "Authorization: Bearer ACCESS-TOKEN"
-H "PayPal-Request-Id: REQUEST-ID"
-d '{
"payment_source": {
"token": {
"id": "VAULT-SETUP-TOKEN",
"type": "SETUP_TOKEN"
}
}
}'sdk.createCardFieldsSavePaymentSession() cannot be used alongside sdk.createCardFieldsPaymentSession() on the same page. Create separate sessions for different use cases:
// Initialize the v6 SDK
const sdk = await window.paypal.createInstance({
clientToken,
components: ["card-fields"]
});
// Option 1: For vaulting (saving cards)
const vaultSession = sdk.createCardFieldsSavePaymentSession();
// Option 2: For immediate payments
const paymentSession = sdk.createCardFieldsPaymentSession();
// Render the same card fields (they can be reused)
const cardFieldsConfig = {
fields: {
number: { selector: '#card-number' },
cvv: { selector: '#card-cvv' },
expirationDate: { selector: '#card-expiry' }
}
};
// You can render with either session
vaultSession.render(cardFieldsConfig);
// OR
// paymentSession.render(cardFieldsConfig);
// Then use the appropriate session based on user intent:
// For vaulting:
async function handleVault() {
const result = await fetch("example.com/create/setup/token");
const setupToken = result.token;
const { state, data } = await vaultSession.submit(setupToken);
// Handle vault result...
}
// For immediate payment:
async function handlePayment() {
const result = await fetch("example.com/create/order");
const orderId = result.id;
const { state, data } = await paymentSession.submit(orderId);
// Handle payment result...If an error prevents checkout, alert the payer that an error has occurred using the console.error() method call.
// Initialize the v6 SDK
const sdk = await window.paypal.createInstance({
clientToken,
components: ["card-fields"]
});
// Create a session (choose based on your use case)
const session = sdk.createCardFieldsSavePaymentSession();
// OR
// const session = sdk.createCardFieldsPaymentSession();
// Render the card fields
session.render({
fields: {
number: { selector: '#card-number' },
cvv: { selector: '#card-cvv' },
expirationDate: { selector: '#card-expiry' }
}
});
// Error handling is now done in your submit function
async function handleSubmit() {
try {
// Your token/order creation logic here
const setupToken = "your-setup-token"; // or orderId for payments
const { state, data } = await session.submit(setupToken);
switch (state) {
case "succeeded": {
// Handle success
break;
}
case "canceled": {
// Handle cancellation
break;
}
case "failed": {
// This replaces your onError callback
console.error('Something went wrong:', data.message);
break;
}
}
} catch (error) {
// This also handles errors (like the onError callback)
console.error('Something went wrong:', error);
}
}When a payer returns to your site, you can show the payer's saved payment methods with the Payment Method Tokens API.
Make the server-side list all payment tokens API call to retrieve payment methods saved to a payer's PayPal-generated customer ID. Based on this list, you can show all saved payment methods to a payer to select during checkout.
Copy and modify the following code.
CUSTOMER-ID to a PayPal-generated customer ID.ACCESS-TOKEN to your sandbox app's access token.REQUEST-ID to a set of unique alphanumeric characters such as a time stamp.curl -L -X GET "https://api-m.sandbox.paypal.com/v3/vault/payment-tokens?customer_id=CUSTOMER-ID"
-H "Content-Type: application/json"
-H "Accept-Language: en_US"
-H "Authorization: Bearer ACCESS-TOKEN"
-H "PayPal-Request-Id: REQUEST-ID" \Display the saved card to the payer and use the Orders API to make another transaction. Use the vault ID the payer selects as an input to the Orders API to capture the payment.
Use supported CSS properties to style the card fields. We recommend showing the card brand and last 4 digits.

The following sample shows a complete back-end integration to save cards for purchase later:
import "dotenv/config";
import express from "express";
const {
PORT = 8888
} = process.env;
const app = express();
app.set("view engine", "ejs");
app.use(express.static("public"));
// Create setup token
app.post("/api/vault/token", async (req, res) => {
try {
// Use your access token to securely generate a setup token
// with an empty payment_source
const vaultResponse = await fetch("https://api-m.sandbox.paypal.com/v3/vault/setup-tokens", {
method: "POST",
body: JSON.stringify({
payment_source: {
card: {}
}
}),
headers: {
Authorization: 'Bearer \${ACCESS-TOKEN}',
"PayPal-Request-Id": Date.now()
}
})
// Return the reponse to the client
res.json(vaultResponse);
} catch (err) {
res.status(500).send(err.message);
}
})
// Create payment token from a setup token
app.post("/api/vault/payment-token", async (req, res) => {
try {
const paymentTokenResult = await fetch("https://api-m.sandbox.paypal.com/v3/vault/payment-tokens", {
method: "POST",
body: {
payment_source: {
token: {
id: req.body.vaultSetupToken,
type: "SETUP_TOKEN"
}
}
},
headers: {
Authorization: 'Bearer \${ACCESS-TOKEN}',
"PayPal-Request-Id": Date.now()
}
})
const paymentMethodToken = paymentTokenResult.id
const customerId = paymentTokenResult.customer.id
await save(paymentMethodToken, customerId)
res.json(captureData);
} catch (err) {
res.status(500).send(err.message);
}
})
const save = async function(paymentMethodToken, customerId) {
// Specify where to save the payment method token
}
app.listen(PORT, () => {
console.log('Server listening at http://localhost:\${PORT}/');
})Use the following card numbers to test transactions in the sandbox:
Test your integration to see if it saves credit and debit cards as expected. Any errors that occur appear in the console.error() method call provided to the CardFields component. Test 3DS success, cancel, and fail cases behave as expected.
session.submit(setupToken) returns succeeded and provides a reference you can store. Update the setup token with card details.customer.id.customer.id to swap the setup-token for a payment-token.payment-token for future use. Test a purchase with the payment token.customer.id.| Issue | Resolution |
|---|---|
| Vault setup is failing | Check session type: Verify you're using createCardFieldsSavePaymentSession() for vault-only flows, not createCardFieldsOneTimePaymentSession(). One-time sessions are for transactions, not vaulting. |
| 3D Secure verification is not completing properly | Check URLs: Ensure you've included return_url and cancel_url in your experience_context when requesting SCA verification. These are required for the 3DS challenge flow to redirect users back to your application. |
| Cannot use vaulted payment methods for transactions | Check vault reference storage: Verify that your server is capturing and persisting the payment token returned in the API response after vaulting. Without storing this reference, you won't be able to use the vaulted card later. |
We recommend creating a page on your site where payers can see their saved payment methods as in the following example:

Go live with your integration.