# Recurring payments (/v5/checkout/recurring)



> **Note:** This page contains content that varies depending on which option is selected: HTML, React. Sections marked with a specific option name apply only when that option is chosen.

> **Warning:** **Deprecation notice:** New integrations should use the [PayPal JavaScript SDK v6](/checkout/integrate).
>
> This integration uses the PayPal JavaScript SDK v5, and it's to troubleshoot existing integrations only.

The [recurring payments module](/v5/checkout/recurring-payments/) 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](/sdk/js/v5/save-without-purchase/paypal/).

## Prerequisites [#prerequisites]

* You must be a buyer or merchant in the United States.
* Must be integrated using the [Payment Method Tokens v3 API](/api/payment-tokens/v3/){/* TODO: verify API link */}.

## How it works [#how-it-works]

1. The buyer signs up for your service and adds PayPal as a payment method.
2. PayPal authenticates the buyer and creates a setup token.
3. The Recurring Payments module shows the buyer details about the recurring payment.
4. The buyer consents to the billing agreement.
5. PayPal creates a payment token that you can use to create and capture payments.

> **HTML:** ## 1. Integrate the front end [#1-integrate-the-front-end]
>
> Configure your script parameters and the layout of the buttons component.
>
> ### Configure your script parameters [#configure-your-script-parameters]
>
> 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.
>
> * Pass a `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.
> * Pass the `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.
>
> > **Note:** Pass values in [`disable-funding`](/sdk/js/configuration/#disable-funding) and [`enable-funding`](/sdk/js/configuration/#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.
>
> ```html lineNumbers 
> <!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&currency=USD&components=buttons"
>             data-sdk-integration-source="developer-studio"
>         ></script>
>         <script src="app.js"></script>
>        
>     </body>
> </html>
> ```
>
> ### Configure the layout of the buttons component [#configure-the-layout-of-the-buttons-component]
>
> 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](/sdk/js/reference/#style).
>
> ```javascript lineNumbers
> 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;
> }
> ```

> **React:** ## 1. Integrate the front end [#1-integrate-the-front-end-1]
>
> 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.
>
> ### Create main.jsx and call it from your HTML [#create-mainjsx-and-call-it-from-your-html]
>
> 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`.
>
> ```javascript lineNumbers 
> 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.
>
> ```html lineNumbers
> <!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>
> ```
>
> ### Configure the layout of the buttons component [#configure-the-layout-of-the-buttons-component-1]
>
> 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](/sdk/js/reference/#style).
>
> ```javascript lineNumbers
> 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;
> ```

## 2. Integrate the back end [#2-integrate-the-back-end]

Take the following steps to set up your backend to support recurring payments.

### Create a setup token [#create-a-setup-token]

To set up the recurring payment module, pass additional fields in the [Create setup token request](/api/payment-tokens/v3/setup-tokens-create/){/* TODO: verify API link */}. 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.

#### Set up a recurring payment type [#set-up-a-recurring-payment-type]

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.

#### Set up a recurring billing plan [#set-up-a-recurring-billing-plan]

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.

#### Customize the billing plan [#customize-the-billing-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.

1. Use paypal as the `payment_source`. Pass additional parameters in the paypal object for your use case and business.
2. Pass the `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.
3. Update the `return_url` value with the URL where the payer is redirected after they approve the flow.
4. Update the `cancel_url` value with the URL where the payer is redirected after they cancel the flow.
5. 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 calling the Create payment token for a given payment source endpoint of the Payment Method Tokens v3 API.

### Get buyer approval [#get-buyer-approval]

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](/api/orders/v2/orders-authorize/).

By default, the setup token expires after 3 days.

### Create a payment token [#create-a-payment-token]

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](/api/payment-tokens/v3/payment-tokens-create){/* TODO: verify API link */}. The endpoint returns the payment source details, links, payment token ID, and customer details.

1. Use `token` as the `payment_source` and complete the rest of the source objects for your use case and business.
2. Pass your setup token ID in the `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.

### Use the payment token with checkout [#use-the-payment-token-with-checkout]

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.

1. Use the ID of your payment method token as the `vault_id`.
2. Specify `intent` to indicate whether to capture a payment immediately or authorize it for a payment later. Use `AUTHORIZE` for Auth-Capture.
3. Add `amount` for the total order.
4. Update `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`.

### Server-side code example [#server-side-code-example]

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 token

Configure payment types and billing terms in the `usage_pattern` and `billing_plan` fields in `createVaultSetupToken`.

node.js

```javascript lineNumbers
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 the integration [#test-the-integration]

Test a purchase as a payer:

1. Select the PayPal button on your recurring payment page.
2. Log in using a personal sandbox account.
3. Note the purchase amount in the PayPal checkout window and approve the purchase with the PayPal button. The PayPal window closes and redirects you to your page, indicating that the transaction was completed.

Confirm the money reached the business account:

1. Log in to the PayPal sandbox using the sandbox business account that received the payment. Remember that the SDK source now uses a sandbox client ID from one of your REST apps, and not the default test ID.
2. In **Recent Activity**, confirm that the sandbox business account received the money, subtracting any fees.
   Log out of the account.

## Troubleshooting [#troubleshooting]

Resolve common issues with PayPal recurring payments integrations.

### 401 Unauthorized [#401-unauthorized]

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](https://developer.paypal.com/dashboard/).
* You're using live credentials against the sandbox environment or vice versa.

### 400 Bad Request [#400-bad-request]

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.

### 422 Unprocessable Entity [#422-unprocessable-entity]

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.

### Setup token status is CREATED but never becomes PAYER\_ACTION\_REQUIRED [#setup-token-status-is-created-but-never-becomes-payer_action_required]

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:

```html
<script src="https://www.paypal.com/sdk/js?client-id=YOUR_ID&vault=true&intent=capture&components=buttons"></script>
```

### 422 — "Please make sure setup token is approved. Please complete the approval flow." [#422--please-make-sure-setup-token-is-approved-please-complete-the-approval-flow]

Symptom: `paymentTokensCreate` returns a 422 after `onApprove` fires.

Causes:

* The buyer closed the PayPal popup without completing approval. `onApprove` should only fire on actual approval. If you're seeing this, you may be calling the endpoint from the wrong callback.
* The setup token was already exchanged for a payment token. Setup tokens are single-use. If you call `paymentTokensCreate` twice with the same setup token ID, the second call fails.
* The `type` field in `paymentSource.token` is missing or wrong. It must be exactly `"SETUP_TOKEN"`.

### 422 — "The PayPal-Request-ID has already been used for another request." [#422--the-paypal-request-id-has-already-been-used-for-another-request]

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.

### 422 — INVALID\_PARAMETER\_VALUE on usage\_pattern [#422--invalid_parameter_value-on-usage_pattern]

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`.

### 404 on capture — Order ID not found [#404-on-capture--order-id-not-found]

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.

### Buttons don't render and no error shown [#buttons-dont-render-and-no-error-shown]

Symptom: `#paypal-button-container` is empty with no console errors.

Causes:

* `components=buttons` is missing from the script URL.
* `vault=true` is missing.
* The container div doesn't exist in the DOM yet when `.render()` is called. Make sure the script runs after the DOM is ready.

### onApprove never fires after buyer approves [#onapprove-never-fires-after-buyer-approves]

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:

```javascript
experienceContext: {
  returnUrl: "http://localhost:8080/return",
  cancelUrl: "http://localhost:8080/cancel",
}
```
