On this page
No Headings
Last updated: June 24, 2026
Deprecation notice: New integrations should use the PayPal JavaScript SDK v6.
This integration uses the PayPal JavaScript SDK v5, and it's to troubleshoot existing integrations only.
The recurring payments module helps you display recurring payment information to the payer before they commit to the payment. Recurring payments are initiated by a merchant based on a schedule or other criteria. Examples include subscriptions and automatic bill payments.
Pay with PayPal supports saving payment methods so that you can charge payers on a recurring basis. To learn more about how to save payment methods, review the Save PayPal for purchase later with the JavaScript SDK guide.
Configure your script parameters and the layout of the buttons component.
Include the <script> tag on any page that shows the PayPal buttons. This script will fetch all the necessary JavaScript to access the buttons on the window object.
client-id and specify which components you want to use. The SDK offers Buttons, Marks, Card Fields, and other components. This sample focuses on the buttons component.currency you want to use for pricing. For this example, we'll use USD. buyer-country and currency are only for use in sandbox testing. Do not use these in production.Pass values in disable-funding and enable-funding to control which funding sources to offer or exclude. For example, if you want to offer Venmo as a payment method, add enable-funding=venmo to your script tag. If you want to exclude credit card payments, add disable-funding=card to your script tag.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>PayPal JS SDK Standard Integration</title>
</head>
<body>
<div id="paypal-button-container"></div>
<p id="result-message"></p>
<!-- Initialize the JS-SDK -->
<script
src="https://www.paypal.com/sdk/js?client-id=test&buyer-country=US¤cy=USD&components=buttons"
data-sdk-integration-source="developer-studio"
></script>
<script src="app.js"></script>
</body>
</html>To override the default style settings for your buttons, use a style object inside the Buttons component. You can lay out the buttons in a horizontal or vertical stack. You can also customize the buttons with different colors and shapes. Read more about how to customize your payment buttons in the style section of the JavaScript SDK v5 reference.
const paypalButtons = window.paypal.Buttons({
style: {
shape: "rect",
layout: "vertical",
color: "gold",
label: "paypal",
},
async createVaultSetupToken() {
try {
const response = await fetch("/api/vault", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
// use the "body" param to optionally pass additional token information
body: JSON.stringify({
payment_source: {
paypal: {
usage_type: "MERCHANT",
experience_context: {
return_url: "https://example.com/returnUrl",
cancel_url: "https://example.com/cancelUrl",
},
},
},
}),
});
const setupTokenData = await response.json();
if (setupTokenData.id) {
return setupTokenData.id;
}
const errorDetail = setupTokenData?.details?.[0];
const errorMessage = errorDetail
? `${errorDetail.issue} ${errorDetail.description} (${setupTokenData.debug_id})`
: JSON.stringify(setupTokenData);
throw new Error(errorMessage);
} catch (error) {
console.error(error);
// resultMessage(`Could not create Setup token...<br><br>${error}`);
}
},
async onApprove(data, actions) {
try {
const response = await fetch(`/api/vault/payment-tokens`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: {
payment_source: {
token: {
id: data.vaultSetupToken,
type: "SETUP_TOKEN",
},
},
},
});
const paymentTokenData = await response.json();
const errorDetail = paymentTokenData?.details?.[0];
if (errorDetail) {
throw new Error(
`${errorDetail.description} (${paymentTokenData.debug_id})`
);
} else {
console.log(
"Payment Token",
paymentTokenData,
JSON.stringify(paymentTokenData, null, 2)
);
}
} catch (error) {
console.error(error);
resultMessage(
`Sorry, could not create tokenized payment source...<br><br>${error}`
);
}
},
});
// Example function to show a result to the user. Your site's UI library can be used instead.
function resultMessage(message) {
const container = document.querySelector("#result-message");
container.innerHTML = message;
}Set up your React front end to integrate recurring payments.
Your app shows the PayPal checkout buttons. When a customer selects a button, your app calls server endpoints to create the order and capture payment.
This example uses a main.jsx file that handles the client-side logic and defines how the PayPal front-end components connect with the back end. Use this file to set up PayPal recurring payments using the JavaScript SDK v5 and handle the payer's interactions with the PayPal checkout button.
You'll need to save the main.jsx file in a folder named /client/react/src.
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App.jsx";
ReactDOM.createRoot(document.getElementById("root")).render(
<React.StrictMode>
<App />
</React.StrictMode>
);Call main.jsx from your HTML file.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>React PayPal JS SDK Standard Integration</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="main.jsx"></script>
</body>
</html>To override the default style settings for your buttons, use a style object inside the Buttons component. You can lay out the buttons in a horizontal or vertical stack. You can also customize the buttons with different colors and shapes. Read more about how to customize your payment buttons in the style section of the JavaScript SDK v5 reference.
import React, { useState } from "react";
import { PayPalScriptProvider, PayPalButtons } from "@paypal/react-paypal-js";
// Renders errors or sucessfull transactions on the screen.
function Message({ content }) {
return <p>{content}</p>;
}
function App() {
const initialOptions = {
"client-id": import.meta.env.PAYPAL_CLIENT_ID,
"enable-funding": "",
"disable-funding": "",
country: "US",
currency: "USD",
"data-page-type": "product-details",
components: "buttons",
"data-sdk-integration-source": "developer-studio",
};
const [message, setMessage] = useState("");
return (
<div className="App">
<PayPalScriptProvider options={initialOptions}>
<PayPalButtons
style={{
shape: "rect",
layout: "vertical",
color: "gold",
label: "paypal",
}}
createVaultSetupToken={async () => {
try {
const response = await fetch("/api/vault", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
// use the "body" param to optionally pass additional token information
body: JSON.stringify({
payment_source: {
paypal: {
usage_type: "MERCHANT",
experience_context: {
return_url:
"https://example.com/returnUrl",
cancel_url:
"https://example.com/cancelUrl",
},
},
},
}),
});
const setupTokenData = await response.json();
if (setupTokenData.id) {
return setupTokenData.id;
}
const errorDetail = setupTokenData?.details?.[0];
const errorMessage = errorDetail
? `${errorDetail.issue} ${errorDetail.description} (${setupTokenData.debug_id})`
: JSON.stringify(setupTokenData);
throw new Error(errorMessage);
} catch (error) {
console.error(error);
// resultMessage(`Could not create Setup token...<br><br>${error}`);
}
}}
onApprove={async (
data,
actions
) => {
try {
const response = await fetch(
`/api/vault/payment-tokens`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: {
payment_source: {
token: {
id: data.vaultSetupToken,
type: "SETUP_TOKEN",
},
},
},
}
);
const paymentTokenData = await response.json();
const errorDetail = paymentTokenData?.details?.[0];
if (errorDetail) {
throw new Error(
`${errorDetail.description} (${paymentTokenData.debug_id})`
);
} else {
console.log(
"Payment Token",
paymentTokenData,
JSON.stringify(paymentTokenData, null, 2)
);
}
} catch (error) {
console.error(error);
resultMessage(
`Sorry, could not create tokenized payment source...<br><br>${error}`
);
}
}}
/>
</PayPalScriptProvider>
<Message content={message} />
</div>
);
}
export default App;Take the following steps to set up your backend to support recurring payments.
To set up the recurring payment module, pass additional fields in the Create setup token request. The buyer reviews and agrees to the recurring billing terms to allow the merchant to charge their PayPal account for future payments.
You'll need to set up a recurring payment type, a recurring billing plan, and then customize the plan.
Pass the recurring payment type in the payment_source.paypal.usage_pattern field of the Create setup token request when you set the payment method. This flags the payment method token for future recurring payments and tailors content in the PayPal flow to show the buyer that this will be a recurring payment.
Recurring payment options include subscriptions, unscheduled payments, and installments. Choose prepaid when the payments are upfront or postpaid when the payments come after the goods or services are delivered.
This shows the buyer a summary of the recurring payment agreement in PayPal. The recurring billing plan details are passed in the payment_source.paypal.billing_plan object of the Create setup token request:
name: The billing plan name is an optional description used to provide further information to the buyer about the service they are purchasing as part of a recurring payment arrangement. This is displayed within the PayPal flow.billing_cycles: The billing cycle is a set of attributes relating to the amount and duration of a billing agreement. Although most recurring payments consist of only one or two billing cycles, PayPal's recurring payments structure supports an array of up to 3 billing cycles to support more complex recurring payment arrangements. A billing cycle may either be a trial or a regular cycle. Trial cycles may be either chargeable or free.one_time_charges: Data that can be provided for a one-time fee that is not part of the ongoing recurring payment arrangement with the payer. Examples of this include setup fees and items ordered when establishing a recurring payment, such as a mobile phone purchased upon signing up for a service plan.Configure your usage_pattern and billing_cycle to match your needs. For more information on billing cycle customizations see the Create a setup token endpoint of the Payment Method Tokens v3 API.
If shipping is required, a user can pass their details in the order.
payment_source. Pass additional parameters in the paypal object for your use case and business.usage_pattern and billing_plan details using the payment_source.paypal object. These are the details that the payer sees on the PayPal review page.return_url value with the URL where the payer is redirected after they approve the flow.cancel_url value with the URL where the payer is redirected after they cancel the flow.Get the buyer's approval for a recurring payment plan by sending a POST request to the Authorize payment for order endpoint of the Create Orders v2 API.
By default, the setup token expires after 3 days.
After the payer completes the approval flow, you can upgrade the setup token to a full payment method token by sending a POST call to the Create payment token for a given payment source endpoint of the Payment Method Tokens v3 API. The endpoint returns the payment source details, links, payment token ID, and customer details.
token as the payment_source and complete the rest of the source objects for your use case and business.payment_source parameter and set the type as SETUP_TOKEN.Store the merchant payer ID aligned with your system to simplify the mapping of payer information between your system and PayPal. This is an optional field that returns the value shared in the response.
After you create a payment method token, use the token instead of the payment method to create a purchase and capture the payment with the Create orders endpoint of the Orders v2 API. Use this to charge your buyers for their recurring payments.
vault_id.intent to indicate whether to capture a payment immediately or authorize it for a payment later. Use AUTHORIZE for Auth-Capture.amount for the total order.stored_credential as the payment source for a vaulted payment method token to provide additional details for recurring transactions that include usage_pattern, payment_initiator, and usage.The following sample shows a complete backend for recurring payments using the PayPal Server SDK. It implements three endpoints:
/api/vault to create a setup token with your recurring billing plan/api/vault/payment-tokens to exchange the approved setup token for a payment token/api/orders to create a merchant-initiated order using the stored tokenConfigure payment types and billing terms in the usage_pattern and billing_plan fields in createVaultSetupToken.
import express from "express";
import "dotenv/config";
import {
ApiError,
CheckoutPaymentIntent,
Client,
Environment,
LogLevel,
OrdersController,
VaultController,
} from "@paypal/paypal-server-sdk";
import bodyParser from "body-parser";
const app = express();
app.use(bodyParser.json());
const {
PAYPAL_CLIENT_ID,
PAYPAL_CLIENT_SECRET,
PORT = 8080,
} = process.env;
const client = new Client({
clientCredentialsAuthCredentials: {
oAuthClientId: PAYPAL_CLIENT_ID,
oAuthClientSecret: PAYPAL_CLIENT_SECRET,
},
timeout: 0,
environment: Environment.Sandbox,
logging: {
logLevel: LogLevel.Info,
logRequest: { logBody: true },
logResponse: { logHeaders: true },
},
});
const ordersController = new OrdersController(client);
const paymentsController = new PaymentsController(client);
const vaultController = new VaultController(client);
/**
* Create a setup token from the given payment source and adds it to the Vault of the associated customer.
* @see /api/payment-tokens/v3/setup-tokens-create
*/
const createVaultSetupToken = async () => {
const collect = {
/* Unique identifier for your request to maintain idempotency */
paypalRequestId: uuidv4(),
body: {
paymentSource: {
paypal: {
usage_type: "MERCHANT",
usage_pattern: "SUBSCRIPTION_PREPAID",
billing_plan: {
billing_cycles: [
{
tenure_type: "REGULAR",
pricing_scheme: {
pricing_model: "FIXED",
price: {
value: "100",
currency_code: "USD",
},
},
frequency: {
interval_unit: "MONTH",
interval_count: "1",
},
total_cycles: "1",
start_date: "2026-03-18",
},
],
one_time_charges: {
product_price: {
value: "10",
currency_code: "USD",
},
total_amount: {
value: 10,
currency_code: "USD",
},
},
product: {
description: "Yearly Membership",
quantity: "1",
},
name: "Company",
},
experience_context: {
return_url: "https://example.com/returnUrl",
cancel_url: "https://example.com/cancelUrl",
},
},
},
},
};
try {
const { result, ...httpResponse } =
await vaultController.setupTokensCreate(collect);
// Get more response info...
// const { statusCode, headers } = httpResponse;
return {
jsonResponse: JSON.parse(body),
httpStatusCode: httpResponse.statusCode,
};
} catch (error) {
if (error instanceof ApiError) {
// const { statusCode, headers } = error;
throw new Error(error.message);
}
}
};
// setupTokensCreate route
app.post("/api/vault", async (req, res) => {
try {
const { jsonResponse, httpStatusCode } = await createVaultSetupToken();
res.status(httpStatusCode).json(jsonResponse);
} catch (error) {
console.error("Failed to set up vault token:", error);
res.status(500).json({ error: "Failed to set up vault token." });
}
});
/**
* Creates a Payment Token from the given payment source and adds it to the Vault of the associated customer.
* @see /api/payment-tokens/v3/payment-tokens-create
*/
const createPaymentToken = async () => {
const collect = {
/* Unique identifier for your request to maintain idempotency */
paypalRequestId: uuidv4(),
body: {
paymentSource: {}, /*Info from customer*/
},
};
try {
const { result, ...httpResponse } =
await vaultController.paymentTokensCreate(collect);
// Get more response info...
// const { statusCode, headers } = httpResponse;
return {
jsonResponse: JSON.parse(body),
httpStatusCode: httpResponse.statusCode,
};
} catch (error) {
if (error instanceof ApiError) {
// const { statusCode, headers } = error;
throw new Error(error.message);
}
}
};
// paymentTokensCreate route
app.post("/api/vault/payment-tokens", async (req, res) => {
try {
const { jsonResponse, httpStatusCode } = await createPaymentToken();
res.status(httpStatusCode).json(jsonResponse);
} catch (error) {
console.error("Failed to create payment token:", error);
res.status(500).json({ error: "Failed to create payment token." });
}
});
/**
* Create an order utilizing the payment token.
* @see /api/orders/v2/orders-create
*/
const createOrder = async (cart) => {
const collect = {
body: {
intent: "CAPTURE",
purchaseUnits: [
{
amount: {
currencyCode: "USD",
value: "100",
},
},
],
payment_source: {
paypal: {
vault_id: "PAYMENT-TOKEN-ID",
stored_credential: {
payment_initiator: "MERCHANT",
usage: "SUBSEQUENT",
usage_pattern: "RECURRING_POSTPAID",
},
},
},
},
prefer: "return=minimal",
};
try {
const { body, ...httpResponse } = await ordersController.ordersCreate(
collect
);
// Get more response info...
// const { statusCode, headers } = httpResponse;
return {
jsonResponse: JSON.parse(body),
httpStatusCode: httpResponse.statusCode,
};
} catch (error) {
if (error instanceof ApiError) {
// const { statusCode, headers } = error;
throw new Error(error.message);
}
}
};
// createOrder route
app.post("/api/orders", async (req, res) => {
try {
// use the cart information passed from the front-end to calculate the order amount detals
const { cart } = req.body;
const { jsonResponse, httpStatusCode } = await createOrder(cart);
res.status(httpStatusCode).json(jsonResponse);
} catch (error) {
console.error("Failed to create order:", error);
res.status(500).json({ error: "Failed to create order." });
}
});
app.listen(PORT, () => {
console.log(`Node server listening at http://localhost:${PORT}/`);
});Test a purchase as a payer:
Confirm the money reached the business account:
Resolve common issues with PayPal recurring payments integrations.
Symptom: Server returns 401 immediately on startup or first vault call.
Causes:
PAYPAL_CLIENT_ID or PAYPAL_CLIENT_SECRET is missing or wrong in your .env. Double-check both values against your app in the Developer Dashboard.Symptom: INVALID_REQUEST with a details array pointing to a field such as /payment_source/paypal/billing_plan/billing_cycles.
Causes:
billing_cycles array is empty or missing required sub-fields such as tenure_type, pricing_scheme, or frequency.interval_count is passed as a string ("1") instead of an integer (1).start_date is in the past.total_cycles is 0 or omitted for a finite plan.Symptom: 422 with an issue of INVALID_PARAMETER_VALUE pointing to the customer_id.
Cause: You're passing a customer.id that doesn't exist in the vault yet, or the format is invalid (must match ^[A-Za-z0-9-_.+=]+$, max 256 chars).
Fix: For first-time buyers, omit customer_id from the setup token request. PayPal will generate one and return it in the response. Store it in your database and pass it on subsequent calls.
Symptom: Token is returned successfully but the PayPal window doesn't prompt the buyer to approve.
Cause: The vault=true parameter is missing from your SDK script tag.
Fix: Add vault=true to the script URL:
<script src="https://www.paypal.com/sdk/js?client-id=YOUR_ID&vault=true&intent=capture&components=buttons"></script>Symptom: paymentTokensCreate returns a 422 after onApprove fires.
Causes:
onApprove should only fire on actual approval. If you're seeing this, you may be calling the endpoint from the wrong callback.paymentTokensCreate twice with the same setup token ID, the second call fails.type field in paymentSource.token is missing or wrong. It must be exactly "SETUP_TOKEN".Symptom: The second call to paymentTokensCreate (or setupTokensCreate) with the same paypalRequestId returns 422.
Cause: paypalRequestId must be unique per request. Reusing it triggers idempotency rejection.
Fix: Ensure uuidv4() is called fresh on every request. Never cache or reuse the value.
Symptom: ordersCreate fails with a details entry pointing to /payment_source/paypal/stored_credential/usage_pattern.
Cause: The usage_pattern value is misspelled or uses an invalid enum.
Fix: usage_pattern values must be consistent between the setup token and the order. Valid values for merchant-initiated recurring charges include IMMEDIATE, DEFERRED, and RECURRING_PREPAID.
Symptom: ordersCapture returns 404 immediately.
Cause: The order was created in sandbox but you're hitting the production endpoint,or vice versa. Also check that order.id is passed correctly. A silent JSON parse failure upstream can result in undefined being interpolated into the URL.
Fix: Log order.id before the capture call. Confirm Environment.Sandbox is set in your Client config.
Symptom: #paypal-button-container is empty with no console errors.
Causes:
components=buttons is missing from the script URL.vault=true is missing..render() is called. Make sure the script runs after the DOM is ready.Symptom: Buyer completes the PayPal flow, popup closes, but onApprove doesn't run.
Cause: return_url and cancel_url in your setup token's experienceContext are pointing to external URLs instead of your local server. The SDK popup needs to redirect back to your domain to fire onApprove.
Fix:
experienceContext: {
returnUrl: "http://localhost:8080/return",
cancelUrl: "http://localhost:8080/cancel",
}