---
title: Integrate subscriptions with PayPal's JavaScript SDK v6
description: Accept recurring PayPal payments using the JavaScript SDK v6.
  Initialize the SDK, create a subscription session, and verify subscription
  status server-side.
badges:
  - type: neutral
    text: JavaScript SDK v6
---

# Integrate subscriptions with PayPal's JavaScript SDK v6 (/subscriptions/integrate/v6)



Configure a subscription plan, initialize the SDK, and create a subscription session using a server-hosted integration. Verify subscription status server-side before you activate it in your system.

## Prerequisites [#prerequisites]

Before starting, ensure you have the following:

* A PayPal [client ID and client secret](/api/rest#1-get-client-id-and-client-secret).
* A PayPal business account enabled to accept PayPal Subscriptions.
* A `plan_id` from an existing subscription plan. If you don't have one, [create a product](/subscriptions/integrate/v5#1-create-product) and [create a subscription plan](/subscriptions/integrate/v5#2-create-subscription-plan).
* A server that creates the subscription. The SDK doesn't create subscriptions. Your server calls the Subscriptions API and returns the `subscriptionId` to the SDK.
* A Node.js or equivalent back end capable of making PayPal API calls.
* HTTPS in production.

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

A subscription integration moves through the following steps:

1. Your server fetches an access token using your client ID and secret.
2. The front end loads the JavaScript SDK v6 core script.
3. The SDK initializes with your client ID using `window.paypal.createInstance()`.
4. The front end checks eligibility using `findEligibleMethods()` with `paymentFlow: "RECURRING_PAYMENT"`.
5. Your code creates a subscription session and reveals the `<paypal-button>`.
6. When the user selects the button, your server calls the Subscriptions API to create a subscription and returns the `subscriptionId`.
7. The buyer approves in the PayPal flow.
8. The `onApprove` callback fires. Your server verifies the subscription status is `ACTIVE`, then fulfills.

## Authentication [#authentication]

PayPal subscriptions use `clientId`, which you fetch from your server endpoint.

> **Note:** **Note:** If your subscription flow includes [Fastlane](/platforms/checkout/fastlane/getstarted), use `clientToken` for authentication. Otherwise, use `clientId`.

## Integrate front end [#integrate-front-end]

Add a subscribe button to your HTML page and connect it to the JavaScript SDK v6 client in `app.js`.

### 1. Load the SDK script [#1-load-the-sdk-script]

Add the JavaScript SDK v6 core script to your HTML page. Use `async` to avoid blocking page render.

```html
<script
  async
  src="https://www.sandbox.paypal.com/web-sdk/v6/core"
  onload="onPayPalWebSdkLoaded()"
></script>
```

> **Note:** **Note:** Switch to `https://www.paypal.com/web-sdk/v6/core` for production.

### 2. Add the HTML structure [#2-add-the-html-structure]

Add the `<paypal-button>` web component with `type="subscribe"`. Keep it hidden until eligibility is confirmed.

```html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Subscribe — JavaScript SDK v6</title>
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <style>
      .buttons-container {
        display: flex;
        flex-direction: column;
        gap: 12px;
        max-width: 400px;
      }
    </style>
  </head>
  <body>
    <h1>Subscribe</h1>
    <div class="buttons-container">
      <!-- Hidden until eligibility is confirmed -->
      <paypal-button id="paypal-button" type="subscribe" hidden></paypal-button>
    </div>
    <script src="app.js"></script>
    <script
      async
      src="https://www.sandbox.paypal.com/web-sdk/v6/core"
      onload="onPayPalWebSdkLoaded()"
    ></script>
  </body>
</html>
```

### 3. Initialize the SDK [#3-initialize-the-sdk]

After the script loads, create an SDK instance using your client ID.

```javascript
async function onPayPalWebSdkLoaded() {
  try {
    const sdkInstance = await window.paypal.createInstance({
      clientId: (await (await fetch("/api/client-id")).json()).clientId, // from your server; never hardcode
      components: ["paypal-subscriptions"],
      pageType: "checkout",
    });

    await checkEligibilityAndSetup(sdkInstance);
  } catch (error) {
    console.error("SDK initialization error:", error);
  }
}
```

The `createInstance()` method accepts these parameters:

| Parameter    | Required | Description                                                  |
| ------------ | -------- | ------------------------------------------------------------ |
| `clientId`   | Yes      | Your PayPal REST app client ID. Fetch from server.           |
| `components` | Yes      | `["paypal-subscriptions"]`. Required for subscription flows. |
| `pageType`   | No       | `"checkout"` is recommended for payment pages.               |

> **Important:** **Important:** The `paypal-billing-agreements` component is deprecated in v6. Use `paypal-subscriptions` instead.

### 4. Check eligibility [#4-check-eligibility]

Always check eligibility before revealing the subscribe button. Pass `paymentFlow: "RECURRING_PAYMENT"` to scope the check to subscription flows. Define `checkEligibilityAndSetup()` as the function `onPayPalWebSdkLoaded()` calls in the previous step.

```javascript
async function checkEligibilityAndSetup(sdkInstance) {
  const paymentMethods = await sdkInstance.findEligibleMethods({
    paymentFlow: "RECURRING_PAYMENT",
    currencyCode: "USD",
  });

  if (paymentMethods.isEligible("paypal")) {
    await setupSubscriptionButton(sdkInstance);
  } else {
    console.log("PayPal Subscriptions not eligible for this session");
  }
}
```

### 5. Create the subscription session and render the button [#5-create-the-subscription-session-and-render-the-button]

Create the subscription session with your callback handlers, then reveal the button and connect the click event.

The session options also accept optional `onShippingAddressChange` and `onShippingOptionsChange` callbacks. These callbacks fire when the buyer changes their shipping address or shipping option in the PayPal flow.

```javascript
async function setupSubscriptionButton(sdkInstance) {
  const session = sdkInstance.createPayPalSubscriptionPaymentSession({

    // Called when the buyer approves the subscription.
    // data contains subscriptionId and, when available, payerId.
    async onApprove(data) {
      // Always verify subscription status server-side before fulfilling
      const res = await fetch(`/api/subscriptions/${data.subscriptionId}`);
      if (!res.ok) {
        console.error("Subscription verification failed:", res.status);
        return;
      }

      const detail = await res.json();
      if (detail.status === "ACTIVE") {
        console.log("Subscription active:", data.subscriptionId);
        // Fulfill in your system: redirect or show success
        window.location.href = "/success";
      }
    },

    // Called when the buyer cancels.
    // data.subscriptionId is present only if a subscription was created.
    onCancel(data) {
      console.log("Subscription canceled:", data.subscriptionId ?? "");
    },

    // Called when an error occurs. The error object includes code and message.
    onError(error) {
      // Do not display error details to buyers.
      console.error("Subscription error:", error.code, error.message);
    },

  });

  // Reveal the button
  const btn = document.getElementById("paypal-button");
  btn.removeAttribute("hidden");

  // On click: create subscription and start the session
  btn.addEventListener("click", async () => {
    try {
      // Pass the Promise; do NOT await before passing to session.start()
      const subscriptionPromise = createSubscription();
      await session.start({ presentationMode: "auto" }, subscriptionPromise);
    } catch (error) {
      console.error("PayPal button click failure:", error);
    }
  });
}
```

> **Important:** **Important:** Do not `await` the `createSubscription()` call before passing it to `session.start()`. The second argument must be a `Promise` that resolves to `{ subscriptionId: string }`. If you await it first, the browser loses the transient activation the popup needs, which can cause the browser to block the popup.

### 6. Call your create-subscription endpoint [#6-call-your-create-subscription-endpoint]

Define `createSubscription()` to call your server’s create-subscription endpoint and return an object containing the subscription ID.

```javascript
// Returns Promise<{ subscriptionId: string }>; do not await before passing to session.start()
async function createSubscription() {
  const res = await fetch("/api/subscriptions/create", { method: "POST" });
  if (!res.ok) {
    throw new Error(`Create subscription failed: ${res.status}`);
  }

  const { subscriptionId } = await res.json();
  if (!subscriptionId) {
    throw new Error("Create subscription response is missing subscriptionId");
  }

  return { subscriptionId };
}
```

## Integrate back end [#integrate-back-end]

Create subscriptions and verify their status using your access token. The following Node.js code is an example. Adapt the routes, session handling, and storage to your own stack. This code lives on your server.

Install the dependencies and set `"type": "module"` in your `package.json` so the `import` statements work:

```bash
npm install express node-fetch express-session
```

Set these environment variables before you start the server: `PAYPAL_CLIENT_ID`, `PAYPAL_CLIENT_SECRET`, and `PAYPAL_PLAN_ID`.

### 1. Get an access token [#1-get-an-access-token]

Exchange your client ID and secret for an access token before calling any Subscriptions API endpoint:

```javascript
import express from "express";
import fetch from "node-fetch";
import session from "express-session";

const app = express();
app.use(express.json());

// Replace with your own session store and a secret from your environment.
// The sample uses the session to remember which subscription this buyer created.
app.use(
  session({
    secret: process.env.SESSION_SECRET || "replace-with-a-long-random-secret",
    resave: false,
    saveUninitialized: true,
  })
);

const PAYPAL_BASE = process.env.PAYPAL_BASE || "https://api-m.sandbox.paypal.com";

async function getAccessToken() {
  const creds = Buffer.from(
    `${process.env.PAYPAL_CLIENT_ID}:${process.env.PAYPAL_CLIENT_SECRET}`
  ).toString("base64");

  const res = await fetch(`${PAYPAL_BASE}/v1/oauth2/token`, {
    method: "POST",
    headers: {
      Authorization: `Basic ${creds}`,
      "Content-Type": "application/x-www-form-urlencoded",
    },
    body: "grant_type=client_credentials",
  });

  if (!res.ok) {
    throw new Error(`Access token request failed: ${res.status}`);
  }

  const { access_token } = await res.json();
  return access_token;
}
```

### 2. Serve the client ID [#2-serve-the-client-id]

Expose your client ID to the front end so you never hardcode it in `app.js`. The client ID is public, so don't return the client secret:

```javascript
// GET /api/client-id
app.get("/api/client-id", (req, res) => {
  res.json({ clientId: process.env.PAYPAL_CLIENT_ID });
});
```

### 3. Create the subscription [#3-create-the-subscription]

Create the subscription using your plan ID and return the subscription ID to the front end:

```javascript
// POST /api/subscriptions/create
app.post("/api/subscriptions/create", async (req, res) => {
  try {
    const accessToken = await getAccessToken();
    const response = await fetch(
      `${PAYPAL_BASE}/v1/billing/subscriptions`,
      {
        method: "POST",
        headers: {
          Authorization: `Bearer ${accessToken}`,
          "Content-Type": "application/json",
          Accept: "application/json",
          Prefer: "return=representation",
        },
        body: JSON.stringify({
          plan_id: process.env.PAYPAL_PLAN_ID,
        }),
      }
    );
    const data = await response.json();
    if (!response.ok || !data.id) {
      console.error("Subscription creation failed:", response.status, data);
      return res.status(502).json({ error: "subscription_create_error" });
    }

    // Replace with your own session handling. Associate the subscription with
    // the signed-in customer so the lookup endpoint can enforce ownership.
    // Track every subscription this session creates, so a second checkout in
    // another tab doesn't overwrite the first.
    req.session.subscriptionIds = [...(req.session.subscriptionIds ?? []), data.id];

    res.json({ subscriptionId: data.id });
  } catch (error) {
    console.error("Subscription creation error:", error);
    res.status(500).json({ error: "subscription_create_error" });
  }
});
```

### 4. Verify the subscription status [#4-verify-the-subscription-status]

After `onApprove` fires, verify the subscription is active before fulfilling.

```javascript
// GET /api/subscriptions/:subscriptionId
app.get("/api/subscriptions/:subscriptionId", async (req, res) => {
  try {
    // Replace with your own authentication and ownership check. Only the
    // customer who created this subscription may look it up.
    if (!req.session?.subscriptionIds?.includes(req.params.subscriptionId)) {
      return res.status(404).json({ error: "subscription_not_found" });
    }

    const accessToken = await getAccessToken();
    const response = await fetch(
      `${PAYPAL_BASE}/v1/billing/subscriptions/${encodeURIComponent(req.params.subscriptionId)}`,
      {
        headers: {
          Authorization: `Bearer ${accessToken}`,
          "Content-Type": "application/json",
        },
      }
    );
    const data = await response.json();
    if (!response.ok) {
      console.error("Subscription verification failed:", response.status, data);
      return res.status(502).json({ error: "subscription_verify_error" });
    }

    // Return only what the browser needs
    res.json({ status: data.status });
  } catch (error) {
    console.error("Subscription verification error:", error);
    res.status(500).json({ error: "subscription_verify_error" });
  }
});
```

### 5. Serve the app and start the server [#5-serve-the-app-and-start-the-server]

Serve your `index.html`, `app.js`, and `success.html` from a `public` folder, then start the server. The `extensions` option lets `/success` resolve to `success.html`:

```javascript
app.use(express.static("public", { extensions: ["html"] }));

const PORT = process.env.PORT || 8080;
app.listen(PORT, () => console.log(`Listening on port ${PORT}`));
```

## Add the success page [#add-the-success-page]

Create `public/success.html`. The buyer lands here after the server confirms the subscription is `ACTIVE`:

```html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Subscription active</title>
    <meta name="viewport" content="width=device-width, initial-scale=1" />
  </head>
  <body>
    <h1>Subscription active</h1>
    <p>Thanks for subscribing.</p>
  </body>
</html>
```

## Security considerations [#security-considerations]

Apply these safeguards when you handle subscription data server-side:

* Never expose your client secret in front-end code.
* Always verify subscription status server-side before fulfilling. Don't rely solely on `onApprove` data.
* Use HTTPS in production.
* Apply CSRF protection on all server-side routes.
* Log PayPal API correlation IDs from response headers for support and debugging.
* Store `subscriptionId` in your system and associate it with the subscriber for lifecycle management.
* Authenticate the caller on every subscription route. Associate each subscription with the customer or session when you create it, and enforce that ownership before you look it up. CSRF protection alone doesn't prevent one customer from reading another's subscription.
* Return only the fields the browser needs, such as `status`. Don't forward the full PayPal subscription record.

## Testing [#testing]

Test the following scenarios in the PayPal sandbox environment before going live:

| Scenario                | Expected result                                                                    |
| ----------------------- | ---------------------------------------------------------------------------------- |
| Successful subscription | `onApprove` fires with `subscriptionId`, server verify returns `status: "ACTIVE"`. |
| Buyer cancels           | `onCancel` fires, non-blocking message shown, retry available.                     |
| Payment error           | `onError` fires, error logged server-side, user-facing message shown.              |
| Invalid plan ID         | Server returns error on subscription creation, front end receives rejection.       |

> **Tip:** Use [sandbox test accounts](https://developer.paypal.com/tools/sandbox/) to simulate buyer interactions. Ensure your sandbox `PAYPAL_PLAN_ID` is set to an active plan in the sandbox environment.

## Production readiness checklist [#production-readiness-checklist]

Confirm each of these items before you switch to production:

* Switch the SDK script URL to `https://www.paypal.com/web-sdk/v6/core`.
* Replace the sandbox API base (`api-m.sandbox.paypal.com`) with production (`api-m.paypal.com`).
* Move all credentials to environment variables. Never hardcode.
* Replace the sandbox `PAYPAL_PLAN_ID` with your production plan ID.
* Add CSRF protection to all back-end routes.
* Verify your Content Security Policy allows PayPal domains.
* Confirm the eligibility check (`paymentFlow: "RECURRING_PAYMENT"`) is in place before rendering the button.
* Confirm subscription status is verified server-side before fulfilling in your system.
* Confirm `onError` and `onCancel` handlers provide user-facing recovery paths.
* Confirm PayPal API correlation IDs are logged for all requests.
* Test keyboard navigation and screen reader accessibility.

## Differences from v5 [#differences-from-v5]

The v6 SDK changes several patterns from v5:

| Area                  | v5                                                                                               | v6                                                                                                                    |
| --------------------- | ------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------- |
| Auth                  | The `client-id` query parameter in the script `src`                                              | The `clientId` value, fetched from your server and passed to the `createInstance()` method                            |
| Script parameters     | The `components=buttons`, `vault=true`, and `intent=subscription` parameters in the script `src` | No subscription parameters in the script `src`. Pass `components: ["paypal-subscriptions"]` to `createInstance()`.    |
| Button rendering      | The `paypal.Buttons({ createSubscription, onApprove }).render()` call                            | Web component: `<paypal-button type="subscribe">`                                                                     |
| Session               | Callbacks passed to `paypal.Buttons()`                                                           | The `createPayPalSubscriptionPaymentSession()` method                                                                 |
| Subscription creation | The `actions.subscription.create({ plan_id })` method inside the `createSubscription` callback   | Your server calls the Subscriptions API. Pass a `Promise` that resolves to `{ subscriptionId }` to `session.start()`. |
| Approval data         | The `data.subscriptionID` property                                                               | The `data.subscriptionId` property, and the `data.payerId` property when available                                    |

## See also [#see-also]

* [Customize subscriptions](/subscriptions/customize)
* [GitHub sample repo for the JavaScript SDK v6](https://github.com/paypal-examples/v6-web-sdk-sample-integration)
* [Integrate subscriptions with the JavaScript SDK v5](/subscriptions/integrate/v5)
* [JavaScript SDK v6 reference](/sdk/js/reference/)
* [Subscriptions REST API reference](/api/subscriptions/v1/)
