react-paypal-js
React components for the PayPal JS SDK
Are you still using the old PayPal JS SDK V5 SDK?
This documentation teaches how to use the latest PayPal JS SDK with react. For the integration using PayPal JS SDK V5 with
PayPalScriptProvider,PayPalButtons,PayPalHostedFields, andBraintreePayPalButtons, see README-PAYPAL-JS-SDK-V5.md.
Why use react-paypal-js?
The Problem
Integrating PayPal into React applications requires careful handling of SDK script loading, payment session management, and UI rendering. Building a robust integration from scratch can lead to issues with timing, state management, and buyer experience.
The Solution
react-paypal-js provides a modern, hooks-based solution that abstracts away the complexities of the PayPal V6 SDK. It enforces best practices by default to ensure buyers get the best possible user experience.
Features
- Modern Hooks API - Fine-grained control over payment sessions with
usePayPalOneTimePaymentSession,useVenmoOneTimePaymentSession, and more - Built-in Eligibility - Automatically check which payment methods are available with
useEligibleMethods() - Web Component Buttons - Use PayPal's optimized
<paypal-button>,<venmo-button>, and<paypal-pay-later-button>web components - Flexible Loading - Support for string token/id, Promise-based token/id, and deferred loading patterns
- TypeScript Support - Complete type definitions for all components and hooks
- SSR Compatible - Built-in hydration handling for server-side rendered applications
Supported Payment Methods
- PayPal - Standard PayPal checkout
- Venmo - Venmo payments
- Pay Later - PayPal's buy now, pay later option
- PayPal Basic Card - Guest card payments without a PayPal account
- PayPal Advanced Card - Card payments with enhanced features and customization options
- PayPal Subscriptions - Recurring billing subscriptions
- PayPal Save - Vault payment methods without purchase
- PayPal Credit - PayPal Credit one-time and save payments
- Google Pay - Native Google Pay button flow through PaymentsClient
- Apple Pay - Native Apple Pay payments (Safari + HTTPS only)
Resources
- PayPal V6 SDK Documentation
- React Sample Integration - Full working example with Node.js backend
- Live Demo - Try the sample integration in sandbox mode
- PayPal Server SDK - For backend integration
- PayPal Developer Dashboard
- PayPal Sandbox Test Accounts
- PayPal Sandbox Card Testing
Installation
npm install @paypal/react-paypal-jsQuick Start
import {
PayPalProvider,
PayPalOneTimePaymentButton,
} from "@paypal/react-paypal-js/sdk-v6";
function App() {
return (
<PayPalProvider
clientId="your-client-id"
environment="sandbox"
components={["paypal-payments"]}
pageType="checkout"
>
<CheckoutPage />
</PayPalProvider>
);
}
function CheckoutPage() {
return (
<PayPalOneTimePaymentButton
createOrder={async () => {
const response = await fetch("/api/create-order", {
method: "POST",
});
const { orderId } = await response.json();
return { orderId };
}}
onApprove={async ({ orderId }: OnApproveDataOneTimePayments) => {
await fetch(`/api/capture-order/${orderId}`, {
method: "POST",
});
console.log("Payment captured!");
}}
/>
);
}PayPalProvider
The PayPalProvider component is the entry point for the V6 SDK. It handles loading the PayPal SDK, creating an instance, and running eligibility checks.
Props
| Prop | Type | Required | Description |
|---|---|---|---|
clientToken |
string | Promise<string> |
* | Client token from your server. Mutually exclusive with clientId. |
clientId |
string | Promise<string> |
* | Client ID from your PayPal app. Mutually exclusive with clientToken. |
components |
Components[] |
No | SDK components to load. Defaults to ["paypal-payments"]. |
pageType |
string |
No | Type of page: "checkout", "product-details", "cart", "product-listing", etc. |
locale |
string |
No | Locale for the SDK (e.g., "en_US"). |
environment |
"sandbox" | "production" |
Yes | Required. SDK environment. clientId does not select the environment in v6 — this prop does. |
merchantId |
string | string[] |
No | PayPal merchant ID(s). |
clientMetadataId |
string |
No | Client metadata ID for tracking. |
partnerAttributionId |
string |
No | Partner attribution ID (BN code). |
shopperSessionId |
string |
No | Shopper session ID for tracking. |
testBuyerCountry |
string |
No | Test buyer country code (sandbox only). |
debug |
boolean |
No | Enable debug mode. |
dataNamespace |
string |
No | Custom namespace for the SDK script data attribute. |
eligibleMethodsResponse |
FindEligiblePaymentMethodsResponse |
No | Server-fetched eligibility response for SDK hydration (see Server-Side Rendering). |
* Either
clientTokenorclientIdis required, but not both. They are mutually exclusive.
Available Components
The components prop accepts an array of the following values:
"paypal-payments"- PayPal and Pay Later buttons"venmo-payments"- Venmo button"paypal-guest-payments"- Guest checkout (card payments)"paypal-subscriptions"- Subscription payments"card-fields"- Card Fields (advanced card payment UI)"googlepay-payments"- Google Pay
With Promise-based Client ID
function App() {
// Memoize to prevent re-fetching on each render
const clientIdPromise = useMemo(() => fetchClientId(), []);
return (
<PayPalProvider
clientId={clientIdPromise}
environment="sandbox"
components={["paypal-payments"]}
pageType="checkout"
>
<CheckoutPage />
</PayPalProvider>
);
}Alternative: With Promise-based Client Token
function App() {
// Memoize to prevent re-fetching on each render
const tokenPromise = useMemo(() => fetchClientToken(), []);
return (
<PayPalProvider
clientToken={tokenPromise}
environment="sandbox"
components={["paypal-payments"]}
pageType="checkout"
>
<CheckoutPage />
</PayPalProvider>
);
}Deferred Loading
function App() {
const [clientId, setClientId] = useState<string>();
useEffect(() => {
fetchClientId().then(setClientId);
}, []);
return (
<PayPalProvider
clientId={clientId}
environment="sandbox"
components={["paypal-payments"]}
pageType="checkout"
>
<CheckoutPage />
</PayPalProvider>
);
}Tracking Loading State
Use the usePayPal hook to access the SDK loading status:
import {
usePayPal,
INSTANCE_LOADING_STATE,
} from "@paypal/react-paypal-js/sdk-v6";
function CheckoutPage() {
const { loadingStatus, error } = usePayPal();
if (loadingStatus === INSTANCE_LOADING_STATE.PENDING) {
return <div className="spinner">Loading PayPal...</div>;
}
if (loadingStatus === INSTANCE_LOADING_STATE.REJECTED) {
return (
<div className="error">Failed to load PayPal SDK: {error?.message}</div>
);
}
return <PayPalOneTimePaymentButton orderId="ORDER-123" />;
}Button Components
PayPalOneTimePaymentButton
Renders a PayPal button for one-time payments.
import { PayPalOneTimePaymentButton } from "@paypal/react-paypal-js/sdk-v6";
<PayPalOneTimePaymentButton
createOrder={async () => {
const response = await fetch("/api/create-order", { method: "POST" });
const { orderId } = await response.json();
return { orderId };
}}
onApprove={async ({ orderId }: OnApproveDataOneTimePayments) => {
await fetch(`/api/capture/${orderId}`, { method: "POST" });
console.log("Payment approved!");
}}
onCancel={(data: OnCancelDataOneTimePayments) =>
console.log("Payment cancelled")
}
onError={(data: OnErrorData) => console.error("Payment error:", data)}
onComplete={(data: OnCompleteData) => console.log("Payment Flow Completed")}
/>;Props:
| Prop | Type | Description |
|---|---|---|
orderId |
string |
Static order ID (alternative to createOrder) |
createOrder |
() => Promise<{ orderId: string }> |
Async function to create an order |
presentationMode |
"auto" | "popup" | "modal" | "redirect" |
Optional. How to present the payment session. Defaults to "auto". |
onApprove |
(data) => void |
Called when payment is approved |
onCancel |
() => void |
Called when buyer cancels |
onError |
(error) => void |
Called on error |
onComplete |
(data) => void |
Called when payment session completes |
type |
"pay" | "checkout" | "buynow" | "donate" | "subscribe" |
Button label type |
disabled |
boolean |
Disable the button |
VenmoOneTimePaymentButton
Renders a Venmo button for one-time payments. Requires "venmo-payments" in the provider's components array.
import { VenmoOneTimePaymentButton } from "@paypal/react-paypal-js/sdk-v6";
<PayPalProvider
clientId={clientId}
environment="sandbox"
components={["paypal-payments", "venmo-payments"]}
pageType="checkout"
>
<VenmoOneTimePaymentButton
createOrder={async () => {
const { orderId } = await createOrder();
return { orderId };
}}
onApprove={(data: OnApproveDataOneTimePayments) =>
console.log("Venmo payment approved!", data)
}
onCancel={(data: OnCancelDataOneTimePayments) =>
console.log("Venmo payment cancelled", data)
}
onError={(data: OnErrorData) => console.error("Venmo payment error:", data)}
onComplete={(data: OnCompleteData) =>
console.log("Venmo payment flow completed", data)
}
/>
</PayPalProvider>;GooglePayOneTimePaymentButton
Renders a native Google Pay button for one-time payments. Requires "googlepay-payments" in the provider's components array.
Google Pay prerequisites:
- Load Google Pay JS in your app HTML shell (for example
public/index.html):
<script async src="https://pay.google.com/gp/p/js/pay.js"></script>- Ensure the script is available before rendering
GooglePayOneTimePaymentButton, since this component depends onwindow.google.payments.api.PaymentsClient.
import {
PayPalProvider,
GooglePayOneTimePaymentButton,
useEligibleMethods,
INSTANCE_LOADING_STATE,
usePayPal,
} from "@paypal/react-paypal-js/sdk-v6";
function GooglePayCheckout() {
const { loadingStatus } = usePayPal();
const { eligiblePaymentMethods, isLoading } = useEligibleMethods({
payload: { currencyCode: "USD" },
});
if (loadingStatus === INSTANCE_LOADING_STATE.PENDING || isLoading) {
return <div>Loading Google Pay...</div>;
}
const googlePayConfig = eligiblePaymentMethods?.isEligible("googlepay")
? eligiblePaymentMethods.getDetails("googlepay").config
: null;
if (!googlePayConfig) {
return <div>Google Pay is not eligible for this buyer.</div>;
}
return (
<GooglePayOneTimePaymentButton
googlePayConfig={googlePayConfig}
transactionInfo={{
countryCode: "US",
currencyCode: "USD",
totalPriceStatus: "FINAL",
totalPrice: "100.00",
}}
createOrder={async () => {
const response = await fetch("/api/create-order", { method: "POST" });
const { orderId } = await response.json();
return { orderId };
}}
onApprove={(data) => console.log("Google Pay approved", data)}
onCancel={() => console.log("Google Pay cancelled")}
onError={(error) => console.error("Google Pay error", error)}
buttonType="pay"
buttonColor="default"
buttonSizeMode="fill"
/>
);
}
function App() {
return (
<PayPalProvider
clientId="your-client-id"
environment="sandbox"
components={["googlepay-payments"]}
pageType="checkout"
>
<GooglePayCheckout />
</PayPalProvider>
);
}Props:
| Prop | Type | Description |
|---|---|---|
googlePayConfig |
GooglePayConfigFromFindEligibleMethods |
Google Pay config returned by eligiblePaymentMethods.getDetails("googlepay") |
transactionInfo |
GooglePayTransactionInfo |
Google Pay transaction details (country, currency, amount, and optional display items) |
createOrder |
() => Promise<{ orderId: string }> |
Async function to create an order |
onApprove |
(data) => void | Promise<void> |
Called when Google Pay payment is approved |
onCancel |
() => void |
Called when buyer cancels the Google Pay sheet |
onError |
(error: Error) => void |
Called on setup or payment errors |
environment |
"TEST" | "PRODUCTION" |
Google Pay environment (default: "TEST") |
buttonType |
"pay" | ... |
Google Pay button type |
buttonColor |
"default" | "black" | "white" |
Google Pay button color |
buttonSizeMode |
"fill" | "static" |
Google Pay button size mode |
buttonLocale |
string |
Google Pay button locale |
disabled |
boolean |
Disable interaction |
PayLaterOneTimePaymentButton
Renders a Pay Later button for financing options. Country code and product code are automatically populated from eligibility data, so eligibility must be fetched first — via useEligibleMethods() client-side (shown below) or the provider's eligibleMethodsResponse prop server-side.
import {
PayLaterOneTimePaymentButton,
useEligibleMethods,
} from "@paypal/react-paypal-js/sdk-v6";
function PayLaterCheckout() {
// Fetch eligibility first (or hydrate server-side via eligibleMethodsResponse)
const { eligiblePaymentMethods, isLoading } = useEligibleMethods({
payload: { purchase_units: [{ amount: { currency_code: "USD" } }] },
});
if (isLoading) {
return <Spinner />;
}
if (!eligiblePaymentMethods?.isEligible("paylater")) {
return null;
}
return (
<PayLaterOneTimePaymentButton
createOrder={async () => {
const { orderId } = await createOrder();
return { orderId };
}}
onApprove={(data: OnApproveDataOneTimePayments) =>
console.log("Pay Later approved!", data)
}
onCancel={(data: OnCancelDataOneTimePayments) =>
console.log("Pay Later cancelled", data)
}
onError={(data: OnErrorData) => console.error("Pay Later error:", data)}
onComplete={(data: OnCompleteData) =>
console.log("Pay Later flow completed", data)
}
/>
);
}PayPalGuestPaymentButton
Renders a guest checkout button for card payments without a PayPal account (Branded Card/Debit Card checkout). Requires "paypal-guest-payments" in the provider's components array.
import { PayPalGuestPaymentButton } from "@paypal/react-paypal-js/sdk-v6";
<PayPalProvider
clientId={clientId}
environment="sandbox"
components={["paypal-payments", "paypal-guest-payments"]}
pageType="checkout"
>
<PayPalGuestPaymentButton
createOrder={async () => {
const { orderId } = await createOrder();
return { orderId };
}}
onApprove={(data: OnApproveDataOneTimePayments) =>
console.log("Guest payment approved!", data)
}
onCancel={(data: OnCancelDataOneTimePayments) =>
console.log("Guest payment cancelled", data)
}
onError={(data: OnErrorData) => console.error("Guest payment error:", data)}
onComplete={(data: OnCompleteData) =>
console.log("Guest payment flow completed", data)
}
/>
</PayPalProvider>;PayPalSavePaymentButton
Renders a button for vaulting a payment method without making a purchase.
import { PayPalSavePaymentButton } from "@paypal/react-paypal-js/sdk-v6";
<PayPalSavePaymentButton
createVaultToken={async () => {
const response = await fetch("/api/create-vault-token", {
method: "POST",
});
const { vaultSetupToken } = await response.json();
return { vaultSetupToken };
}}
onApprove={({ vaultSetupToken }: OnApproveDataSavePayments) => {
console.log("Payment method saved:", vaultSetupToken);
}}
onCancel={(data: OnCancelDataSavePayments) =>
console.log("Save payment cancelled", data)
}
onError={(data: OnErrorData) => console.error("Save payment error:", data)}
onComplete={(data: OnCompleteData) =>
console.log("Save payment flow completed", data)
}
/>;PayPalSubscriptionButton
Renders a PayPal button for subscription payments. Requires "paypal-subscriptions" in the provider's components array.
import { PayPalSubscriptionButton } from "@paypal/react-paypal-js/sdk-v6";
<PayPalProvider
clientId={clientId}
environment="sandbox"
components={["paypal-subscriptions"]}
pageType="checkout"
>
<PayPalSubscriptionButton
createSubscription={async () => {
const response = await fetch("/api/create-subscription", {
method: "POST",
});
const { subscriptionId } = await response.json();
return { subscriptionId };
}}
onApprove={(data: OnApproveDataOneTimePayments) =>
console.log("Subscription approved:", data)
}
onCancel={(data: OnCancelDataOneTimePayments) =>
console.log("Subscription cancelled", data)
}
onError={(data: OnErrorData) => console.error("Subscription error:", data)}
onComplete={(data: OnCompleteData) =>
console.log("Subscription flow completed", data)
}
/>
</PayPalProvider>;PayPalCreditOneTimePaymentButton
Renders a PayPal Credit button for one-time payments. The countryCode is automatically populated from eligibility data, so eligibility must be fetched first — via useEligibleMethods() client-side (shown below) or the provider's eligibleMethodsResponse prop server-side.
import {
PayPalCreditOneTimePaymentButton,
useEligibleMethods,
} from "@paypal/react-paypal-js/sdk-v6";
function CreditCheckout() {
// Fetch eligibility first (or hydrate server-side via eligibleMethodsResponse)
const { eligiblePaymentMethods, isLoading } = useEligibleMethods({
payload: { purchase_units: [{ amount: { currency_code: "USD" } }] },
});
if (isLoading) {
return <Spinner />;
}
if (!eligiblePaymentMethods?.isEligible("credit")) {
return null;
}
return (
<PayPalCreditOneTimePaymentButton
createOrder={async () => {
const response = await fetch("/api/create-order", { method: "POST" });
const { orderId } = await response.json();
return { orderId };
}}
onApprove={({ orderId }: OnApproveDataOneTimePayments) =>
console.log("Credit payment approved:", orderId)
}
onCancel={(data: OnCancelDataOneTimePayments) =>
console.log("Credit payment cancelled", data)
}
onError={(data: OnErrorData) =>
console.error("Credit payment error:", data)
}
onComplete={(data: OnCompleteData) =>
console.log("Credit payment flow completed", data)
}
/>
);
}PayPalCreditSavePaymentButton
Renders a PayPal Credit button for saving a credit payment method (vaulting). The countryCode is automatically populated from eligibility data, so eligibility must be fetched first — via useEligibleMethods() client-side (shown below) or the provider's eligibleMethodsResponse prop server-side.
import {
PayPalCreditSavePaymentButton,
useEligibleMethods,
} from "@paypal/react-paypal-js/sdk-v6";
function CreditSaveCheckout() {
// Fetch eligibility first (or hydrate server-side via eligibleMethodsResponse)
const { eligiblePaymentMethods, isLoading } = useEligibleMethods({
payload: { purchase_units: [{ amount: { currency_code: "USD" } }] },
});
if (isLoading) {
return <Spinner />;
}
if (!eligiblePaymentMethods?.isEligible("credit")) {
return null;
}
return (
<PayPalCreditSavePaymentButton
createVaultToken={async () => {
const response = await fetch("/api/create-vault-token", {
method: "POST",
});
const { vaultSetupToken } = await response.json();
return { vaultSetupToken };
}}
onApprove={(data: OnApproveDataSavePayments) =>
console.log("Credit saved:", data)
}
onCancel={(data: OnCancelDataSavePayments) =>
console.log("Credit save cancelled", data)
}
onError={(data: OnErrorData) => console.error("Credit save error:", data)}
onComplete={(data: OnCompleteData) =>
console.log("Credit save flow completed", data)
}
/>
);
}ApplePayOneTimePaymentButton
Renders Apple's native <apple-pay-button> web component and manages the full Apple Pay payment flow — including merchant validation, payment authorization, and order confirmation — via the PayPal SDK.
Requirements:
- Safari browser (macOS 10.12+ / iOS 10+)
- HTTPS connection
- Apple Pay configured on the user's device
components={["applepay-payments"]}inPayPalProvider- Apple Pay JS SDK loaded via a script tag in your HTML:
<script
crossorigin
src="https://applepay.cdn-apple.com/jsapi/1.latest/apple-pay-sdk.js"
></script>Integration steps:
- Check
window.ApplePaySession?.canMakePayments()— only render the button if this returnstrue. Wrap intry-catchbecause it throws on non-HTTPS connections. - Call
useEligibleMethods()to fetch eligibility and obtainapplePayConfigfromgetDetails("applepay").config. - Pass
applePayConfigexplicitly to the component — it is a required prop.
import {
PayPalProvider,
ApplePayOneTimePaymentButton,
useEligibleMethods,
} from "@paypal/react-paypal-js/sdk-v6";
async function createOrder() {
const response = await fetch("/api/paypal/create-order", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
items: [{ id: "item-1", quantity: 1 }],
}),
});
const data = await response.json();
return { orderId: data.id };
}
async function onApprove(data) {
// confirmOrder is handled internally by the hook.
// Capture the order using the ID from the confirmation response.
const orderId = data.approveApplePayPayment.id;
const response = await fetch(`/api/paypal/capture/${orderId}`, {
method: "POST",
});
const result = await response.json();
console.log("Apple Pay payment captured:", result);
}
function ApplePayCheckout() {
// Step 1: Check if Apple Pay is supported by the browser/device.
// canMakePayments() throws on non-HTTPS, so wrap in try-catch.
let canUseApplePay = false;
try {
canUseApplePay =
typeof window !== "undefined" &&
!!window.ApplePaySession?.canMakePayments();
} catch {
// Not available (e.g., non-HTTPS environment)
}
// Step 2: Fetch eligibility.
// Note: hooks must be called unconditionally (React rules of hooks).
// To avoid the eligibility API call on unsupported browsers, split the
// check and the button into separate components in your app.
const { eligiblePaymentMethods, isLoading, error } = useEligibleMethods({
payload: { currencyCode: "USD" },
});
if (!canUseApplePay) {
return <div>Apple Pay is not available in this browser.</div>;
}
if (isLoading) {
return <div>Loading...</div>;
}
if (error) {
return <div>Error: {error.message}</div>;
}
// Step 3: Check merchant eligibility and get config.
const isEligible = eligiblePaymentMethods?.isEligible("applepay");
if (!isEligible) {
return <div>Apple Pay is not eligible.</div>;
}
const applePayConfig = eligiblePaymentMethods?.getDetails("applepay")?.config;
if (!applePayConfig) {
return null;
}
return (
<ApplePayOneTimePaymentButton
applePayConfig={applePayConfig}
paymentRequest={{
countryCode: "US",
currencyCode: "USD",
requiredBillingContactFields: [
"name",
"phone",
"email",
"postalAddress",
],
requiredShippingContactFields: [],
total: {
label: "Demo (Card is not charged)",
amount: "20.00",
type: "final",
},
}}
createOrder={createOrder}
onApprove={onApprove}
onCancel={() => console.log("Apple Pay cancelled")}
onError={(error) => console.error("Apple Pay error:", error)}
applePaySessionVersion={4}
buttonstyle="black"
type="buy"
/>
);
}
export default function App() {
return (
<PayPalProvider
clientId="YOUR_CLIENT_ID"
environment="sandbox"
components={["applepay-payments"]}
pageType="checkout"
>
<ApplePayCheckout />
</PayPalProvider>
);
}Props:
| Prop | Type | Required | Description |
|---|---|---|---|
applePayConfig |
ApplePayConfig |
Yes | Config object from useEligibleMethods().getDetails("applepay").config |
paymentRequest |
ApplePayPaymentRequest |
Yes | Apple Pay payment request (countryCode, currencyCode, total, etc.) |
createOrder |
() => Promise<{ orderId: string }> |
Yes | Called during authorization to create the PayPal order |
onApprove |
(data: ConfirmOrderResponse) => void |
Yes | Called after payment confirmation; use data.approveApplePayPayment.id to capture |
onCancel |
() => void |
No | Called when the buyer dismisses the payment sheet |
onError |
(error: Error) => void |
No | Called on errors (merchant validation failure, network error, etc.) |
applePaySessionVersion |
number |
No | Apple Pay JS API version passed to ApplePaySession (minimum: 4) |
buttonstyle |
"black" | "white" | "white-outline" |
No | Visual style of the Apple Pay button |
type |
"pay" | "buy" | "set-up" | "donate" | "check-out" | "book" | "subscribe" |
No | Label displayed on the button |
locale |
string |
No | Locale for the button label (e.g., "en", "fr", "ja") |
disabled |
boolean |
No | Disables the button |
Key differences from other PayPal buttons:
- No
presentationMode— Apple controls the native payment sheet UI - No eager order creation (
orderIdprop) — orders are always created lazily during payment authorization applePayConfigis required and must be obtained fromuseEligibleMethods()onApprovereceivesConfirmOrderResponse— capture the order usingdata.approveApplePayPayment.id
Braintree PayPal Integration
Braintree merchants use BraintreePayPalProvider instead of PayPalProvider to integrate PayPal via Braintree's paypalCheckoutV6 module. This provider initializes the Braintree client, creates a PayPal Checkout V6 instance, and loads the PayPal SDK — then exposes the instance to child components and hooks via React context.
Resources:
- Braintree PayPalCheckoutV6 API Reference — full method signatures, options, and type definitions
- Sample Integration Repo — working Braintree + React example with setup instructions
- Braintree Server SDK Guide — server-side nonce processing
Prerequisites
- A Braintree merchant account with PayPal enabled
- A Braintree client token generated server-side via the Braintree SDK. See the sample integration's gateway setup and client token route handler for a Node.js example.
- The Braintree Web
clientandpaypal-checkout-v6scripts loaded before rendering:
<script src="https://js.braintreegateway.com/web/3.142.0/js/client.min.js"></script>
<script src="https://js.braintreegateway.com/web/3.142.0/js/paypal-checkout-v6.min.js"></script>BraintreePayPalProvider
Wraps child components with Braintree context. On mount it validates the namespace, creates a Braintree client instance, creates a paypalCheckoutV6 instance, and calls loadPayPalSDK(). On unmount it calls teardown() to release resources.
import { useState, useEffect } from "react";
import { BraintreePayPalProvider } from "@paypal/react-paypal-js/sdk-v6";
declare global {
interface Window {
braintree: BraintreeV6Namespace;
}
}
function App() {
const [clientToken, setClientToken] = useState<string | undefined>(undefined);
useEffect(() => {
fetch("/auth/browser-safe-client-token")
.then((res) => res.json())
.then(({ clientToken }) => setClientToken(clientToken));
}, []);
if (!clientToken) {
return <div>Loading...</div>;
}
return (
<BraintreePayPalProvider
namespace={window.braintree}
braintreeClientToken={clientToken}
>
<CheckoutPage />
</BraintreePayPalProvider>
);
}Props:
| Prop | Type | Required | Description |
|---|---|---|---|
namespace |
BraintreeV6Namespace |
Yes | The braintree global namespace — must expose client.create and paypalCheckoutV6.create functions |
braintreeClientToken |
string | undefined |
Yes | Client token from your server (generated via the Braintree SDK) |
children |
ReactNode |
Yes | Child components |
Note: The
namespaceprop must have referential stability across renders. An unstable reference (e.g., creating the object inline) will cause re-initialization on every render. Use a module-level constant,useRef, oruseMemo.
BraintreePayPalOneTimePaymentButton
Renders a <paypal-button> web component for one-time Braintree PayPal payments. Internally uses useBraintreePayPalOneTimePaymentSession to create and start payment sessions.
import {
BraintreePayPalProvider,
BraintreePayPalOneTimePaymentButton,
useBraintreePayPal,
} from "@paypal/react-paypal-js/sdk-v6";
import type { BraintreeApprovalData } from "@paypal/react-paypal-js/sdk-v6";
function CheckoutPage() {
const { braintreePayPalCheckoutInstance } = useBraintreePayPal();
return (
<BraintreePayPalOneTimePaymentButton
amount="100.00"
currency="USD"
intent="capture"
type="pay"
onApprove={async (data: BraintreeApprovalData) => {
const { nonce } =
await braintreePayPalCheckoutInstance!.tokenizePayment(data);
// Send nonce to your server
await fetch("/api/braintree/checkout", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ nonce }),
});
}}
onCancel={(data) => console.log("Cancelled", data)}
onError={(err) => console.error("Error", err)}
/>
);
}
function App() {
const [clientToken, setClientToken] = useState<string | undefined>(undefined);
useEffect(() => {
fetch("/auth/browser-safe-client-token")
.then((res) => res.json())
.then(({ clientToken }) => setClientToken(clientToken));
}, []);
if (!clientToken) {
return <div>Loading...</div>;
}
return (
<BraintreePayPalProvider
namespace={window.braintree}
braintreeClientToken={clientToken}
>
<CheckoutPage />
</BraintreePayPalProvider>
);
}Props:
| Prop | Type | Required | Description |
|---|---|---|---|
amount |
string |
Yes | Payment amount (e.g., "100.00") |
currency |
string |
Yes | ISO 4217 currency code (e.g., "USD") |
onApprove |
(data: BraintreeApprovalData) => Promise<void> |
Yes | Called when buyer approves — tokenize the payment here |
intent |
"authorize" | "capture" | "order" |
No | Payment intent (default: "capture") |
commit |
boolean |
No | true for "Pay Now", false for "Continue" |
offerCredit |
boolean |
No | Offer PayPal Credit as default funding |
onCancel |
(data: BraintreeOnCancelData) => void |
No | Called when buyer cancels |
onError |
(err: Error) => void |
No | Called on errors |
onShippingAddressChange |
(data: BraintreeShippingAddressChangeData) => Promise<void> |
No | Called when buyer changes shipping address |
onShippingOptionsChange |
(data: BraintreeShippingOptionsChangeData) => Promise<void> |
No | Called when buyer selects a shipping option |
lineItems |
BraintreeLineItem[] |
No | Line items for the transaction |
shippingOptions |
BraintreeShippingOption[] |
No | Available shipping options |
amountBreakdown |
BraintreeAmountBreakdown |
No | Breakdown of the total amount (item total, shipping, tax, etc.) |
userAuthenticationEmail |
string |
No | Pre-fill the PayPal login email |
displayName |
string |
No | Merchant name displayed in the PayPal lightbox |
presentationMode |
BraintreePresentationMode |
No | UI mode: "auto", "popup", "modal", "redirect", etc. |
returnUrl |
string |
No | Return URL (required for "direct-app-switch" presentation mode) |
cancelUrl |
string |
No | Cancel URL (required for "direct-app-switch" presentation mode) |
type |
"pay" | "checkout" | "buynow" | "donate" | "subscribe" |
No | Button label type (default: "pay") |
disabled |
boolean |
No | Disable the button |
BraintreePayPalBillingAgreementButton
Renders a <paypal-button> for vault-only flows — saving a buyer's PayPal account as a payment method without an immediate charge. Supports subscription plans via planType and planMetadata.
import {
BraintreePayPalBillingAgreementButton,
useBraintreePayPal,
} from "@paypal/react-paypal-js/sdk-v6";
import type { BraintreeApprovalData } from "@paypal/react-paypal-js/sdk-v6";
function BillingAgreementButton() {
const { braintreePayPalCheckoutInstance } = useBraintreePayPal();
return (
<BraintreePayPalBillingAgreementButton
type="subscribe"
billingAgreementDescription="Monthly subscription to Premium"
planType="SUBSCRIPTION"
planMetadata={{
currencyIsoCode: "USD",
name: "Premium Plan",
billingCycles: [
{
billingFrequency: 1,
billingFrequencyUnit: "MONTH",
numberOfExecutions: 0,
sequence: 1,
startDate: "2026-07-01T00:00:00Z",
trial: false,
pricingScheme: { pricingModel: "FIXED", price: "9.99" },
},
],
}}
onApprove={async (data: BraintreeApprovalData) => {
const { nonce } =
await braintreePayPalCheckoutInstance!.tokenizePayment(data);
await fetch("/api/braintree/vault", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ nonce }),
});
}}
onCancel={(data) => console.log("Cancelled", data)}
onError={(err) => console.error("Error", err)}
/>
);
}Props:
| Prop | Type | Required | Description |
|---|---|---|---|
onApprove |
(data: BraintreeApprovalData) => Promise<void> |
Yes | Called when buyer approves — tokenize with data.billingToken |
billingAgreementDescription |
string |
No | Description shown to the buyer (e.g., "Monthly subscription") |
planType |
"RECURRING" | "SUBSCRIPTION" | "UNSCHEDULED" | "INSTALLMENTS" |
No | Type of billing plan |
planMetadata |
BraintreePlanMetadata |
No | Subscription plan details including billing cycles |
amount |
string |
No | Amount for vault-with-purchase flows |
currency |
string |
No | Currency for vault-with-purchase flows |
offerCredit |
boolean |
No | Offer PayPal Credit |
userAction |
"CONTINUE" | "COMMIT" | "SETUP_NOW" |
No | Button action label |
displayName |
string |
No | Merchant name in the PayPal lightbox |
shippingAddressOverride |
Record<string, unknown> |
No | Pre-collected shipping address |
onCancel |
(data: BraintreeOnCancelData) => void |
No | Called when buyer cancels |
onError |
(err: Error) => void |
No | Called on errors |
presentationMode |
BraintreePresentationMode |
No | UI mode: "auto", "popup", "modal", "redirect", etc. |
returnUrl |
string |
No | Return URL (required for app-switch modes) |
cancelUrl |
string |
No | Cancel URL (required for app-switch modes) |
type |
"pay" | "checkout" | "buynow" | "donate" | "subscribe" |
No | Button label type (default: "pay") |
disabled |
boolean |
No | Disable the button |
BraintreePayPalCheckoutWithVaultButton
Renders a <paypal-button> for a combined flow — charging the buyer and saving their payment method in a single transaction (one-time payment + billing agreement consent).
import {
BraintreePayPalCheckoutWithVaultButton,
useBraintreePayPal,
} from "@paypal/react-paypal-js/sdk-v6";
import type { BraintreeApprovalData } from "@paypal/react-paypal-js/sdk-v6";
function CheckoutWithVaultButton() {
const { braintreePayPalCheckoutInstance } = useBraintreePayPal();
return (
<BraintreePayPalCheckoutWithVaultButton
amount="49.99"
currency="USD"
intent="capture"
type="pay"
billingAgreementDetails={{
description: "Monthly subscription to Products!",
}}
onApprove={async (data: BraintreeApprovalData) => {
const { nonce } =
await braintreePayPalCheckoutInstance!.tokenizePayment(data);
await fetch("/api/braintree/checkout-and-vault", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ nonce }),
});
}}
onCancel={() => console.log("Cancelled")}
onError={(err) => console.error("Error", err)}
/>
);
}Props:
| Prop | Type | Required | Description |
|---|---|---|---|
amount |
string |
Yes | Payment amount (e.g., "49.99") |
currency |
string |
Yes | ISO 4217 currency code |
onApprove |
(data: BraintreeApprovalData) => Promise<void> |
Yes | Called when buyer approves — tokenize the payment here |
intent |
"authorize" | "capture" | "order" |
No | Payment intent (default: "capture") |
commit |
boolean |
No | true for "Pay Now", false for "Continue" |
billingAgreementDetails |
{ description?: string } |
No | Billing agreement details shown to the buyer |
onCancel |
() => void |
No | Called when buyer cancels |
onError |
(err: Error) => void |
No | Called on errors |
onShippingAddressChange |
(data: BraintreeShippingAddressChangeData) => Promise<void> |
No | Called when buyer changes shipping address |
onShippingOptionsChange |
(data: BraintreeShippingOptionsChangeData) => Promise<void> |
No | Called when buyer selects a shipping option |
lineItems |
BraintreeLineItem[] |
No | Line items for the transaction |
shippingOptions |
BraintreeShippingOption[] |
No | Available shipping options |
amountBreakdown |
BraintreeAmountBreakdown |
No | Breakdown of the total amount |
userAuthenticationEmail |
string |
No | Pre-fill the PayPal login email |
displayName |
string |
No | Merchant name in the PayPal lightbox |
presentationMode |
BraintreePresentationMode |
No | UI mode: "auto", "popup", "modal", "redirect", etc. |
returnUrl |
string |
No | Return URL (required for "direct-app-switch" presentation mode) |
cancelUrl |
string |
No | Cancel URL (required for "direct-app-switch" presentation mode) |
type |
"pay" | "checkout" | "buynow" | "donate" | "subscribe" |
No | Button label type (default: "pay") |
disabled |
boolean |
No | Disable the button |
BraintreePayPalPayLaterButton
Renders a <paypal-pay-later-button> web component for Braintree PayPal Pay Later (Buy Now, Pay Later) financing. Internally uses useBraintreePayPalPayLaterSession to create and start Pay Later sessions.
Unlike the other Braintree buttons, this button requires eligibility data. It reads countryCode and productCode from useBraintreePayPal().eligiblePaymentMethods, which is populated by useBraintreeEligibleMethods. Fetch eligibility first — without it the button renders hidden (display: none).
import {
BraintreePayPalPayLaterButton,
useBraintreePayPal,
useBraintreeEligibleMethods,
INSTANCE_LOADING_STATE,
} from "@paypal/react-paypal-js/sdk-v6";
import type { BraintreeApprovalData } from "@paypal/react-paypal-js/sdk-v6";
function PayLaterCheckout() {
const { loadingStatus, braintreePayPalCheckoutInstance } =
useBraintreePayPal();
// Fetch eligibility before rendering the button
const { eligiblePaymentMethods, isLoading } = useBraintreeEligibleMethods({
amount: "100.00",
currency: "USD", // required
countryCode: "US",
paymentFlow: "ONE_TIME_PAYMENT",
});
if (isLoading || loadingStatus === INSTANCE_LOADING_STATE.PENDING) {
return <Spinner />;
}
if (!eligiblePaymentMethods?.paylater) {
return null;
}
return (
<BraintreePayPalPayLaterButton
amount="100.00"
currency="USD"
onApprove={async (data: BraintreeApprovalData) => {
const { nonce } =
await braintreePayPalCheckoutInstance!.tokenizePayment(data);
// Send nonce to your server
await fetch("/api/braintree/checkout", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ nonce }),
});
}}
onCancel={() => console.log("Cancelled")}
onError={(err) => console.error("Error", err)}
/>
);
}Render PayLaterCheckout inside a BraintreePayPalProvider (see BraintreePayPalProvider).
Props:
| Prop | Type | Required | Description |
|---|---|---|---|
amount |
string |
Yes | Payment amount (e.g., "100.00") |
currency |
string |
Yes | ISO 4217 currency code (e.g., "USD") |
onApprove |
(data: BraintreeApprovalData) => Promise<void> |
Yes | Called when buyer approves — tokenize the payment here |
intent |
"authorize" | "capture" | "order" |
No | Payment intent (default: "capture") |
onCancel |
() => void |
No | Called when buyer cancels |
onComplete |
() => void |
No | Called when the payment flow completes |
onError |
(err: Error) => void |
No | Called on errors |
onShippingAddressChange |
(data: BraintreeShippingAddressChangeData) => Promise<void> |
No | Called when buyer changes shipping address |
onShippingOptionsChange |
(data: BraintreeShippingOptionsChangeData) => Promise<void> |
No | Called when buyer selects a shipping option |
lineItems |
BraintreeLineItem[] |
No | Line items for the transaction |
shippingOptions |
BraintreeShippingOption[] |
No | Available shipping options |
shippingCallbackUrl |
string |
No | URL for server-side shipping callbacks |
shippingAddressOverride |
BraintreeShippingAddressOverride |
No | Pre-collected shipping address |
amountBreakdown |
BraintreeAmountBreakdown |
No | Breakdown of the total amount (item total, shipping, tax, etc.) |
contactPreference |
"NO_CONTACT_INFO" | "RETAIN_CONTACT_INFO" | "UPDATE_CONTACT_INFO" |
No | How buyer contact information is handled |
userAuthenticationEmail |
string |
No | Pre-fill the PayPal login email |
returnUrl |
string |
No | Return URL (required for "direct-app-switch" presentation mode) |
cancelUrl |
string |
No | Cancel URL (required for "direct-app-switch" presentation mode) |
displayName |
string |
No | Merchant name displayed in the PayPal lightbox |
presentationMode |
BraintreePresentationMode |
No | UI mode: "auto", "popup", "modal", "redirect", etc. |
disabled |
boolean |
No | Disable the button |
Braintree Hooks
useBraintreePayPal()
Accesses the Braintree PayPal context. Returns the checkout instance, loading status, and error state. Must be used within a BraintreePayPalProvider.
import {
useBraintreePayPal,
INSTANCE_LOADING_STATE,
} from "@paypal/react-paypal-js/sdk-v6";
function CustomCheckout() {
const { braintreePayPalCheckoutInstance, loadingStatus, error, isHydrated } =
useBraintreePayPal();
if (loadingStatus === INSTANCE_LOADING_STATE.PENDING) {
return <div>Initializing Braintree...</div>;
}
if (error) {
return <div>Error: {error.message}</div>;
}
// Use braintreePayPalCheckoutInstance directly for custom flows
}Returns:
| Property | Type | Description |
|---|---|---|
braintreePayPalCheckoutInstance |
BraintreePayPalCheckoutInstance | null |
The checkout instance (null while loading) |
eligiblePaymentMethods |
BraintreeEligibilityResult | null |
Eligibility cached by useBraintreeEligibleMethods (null until fetched) |
loadingStatus |
INSTANCE_LOADING_STATE |
"pending", "resolved", or "rejected" |
error |
Error | null |
Initialization error, if any |
isHydrated |
boolean |
true after client-side hydration |
useBraintreeEligibleMethods(props)
Fetches Braintree PayPal eligibility for the given checkout options by calling findEligibleMethods() on the shared checkout instance, and caches the result in BraintreePayPalProvider context (where buttons read it via useBraintreePayPal().eligiblePaymentMethods). Fetches are deduplicated by (instance, options) and re-run when the options change. Required before rendering eligibility-gated buttons such as BraintreePayPalPayLaterButton.
import {
useBraintreePayPal,
useBraintreeEligibleMethods,
BraintreePayPalOneTimePaymentButton,
BraintreePayPalPayLaterButton,
INSTANCE_LOADING_STATE,
} from "@paypal/react-paypal-js/sdk-v6";
function CheckoutButtons() {
const { loadingStatus, braintreePayPalCheckoutInstance } =
useBraintreePayPal();
const { eligiblePaymentMethods, isLoading, error } =
useBraintreeEligibleMethods({
amount: "100.00",
currency: "USD",
countryCode: "US",
paymentFlow: "ONE_TIME_PAYMENT",
});
if (isLoading || loadingStatus === INSTANCE_LOADING_STATE.PENDING) {
return <Spinner />;
}
if (error) {
return <div>Error: {error.message}</div>;
}
return (
<>
<BraintreePayPalOneTimePaymentButton
amount="100.00"
currency="USD"
// onApprove not shown in this example
// see other examples for usage of tokenizePayment in onApprove
onApprove={onApprove}
/>
{eligiblePaymentMethods?.paylater && (
<BraintreePayPalPayLaterButton
amount="100.00"
currency="USD"
onApprove={onApprove}
/>
)}
</>
);
}Props (BraintreeFindEligibleMethodsOptions):
| Prop | Type | Required | Description |
|---|---|---|---|
currency |
string |
Yes | ISO 4217 currency code (e.g., "USD") |
amount |
string |
No | Checkout amount used for eligibility checks |
countryCode |
string |
No | Buyer country code (e.g., "US") |
paymentFlow |
"ONE_TIME_PAYMENT" | "VAULT_WITH_PAYMENT" | "VAULT_WITHOUT_PAYMENT" | "RECURRING_PAYMENT" |
No | The flow eligibility is being checked for |
Returns:
| Property | Type | Description |
|---|---|---|
eligiblePaymentMethods |
BraintreeEligibilityResult | null |
Eligibility result, or null until fetched |
isLoading |
boolean |
true while the instance is initializing or eligibility is being fetched |
error |
Error | null |
Fetch- or provider-level error, if any |
BraintreeEligibilityResult exposes the booleans paypal, paylater, and credit, plus getDetails(method) which returns the countryCode and productCode the buttons consume.
useBraintreePayPalOneTimePaymentSession(props)
Creates a one-time payment session. Returns a handleClick function to start the PayPal flow. Accepts the same props as BraintreePayPalOneTimePaymentButton (minus type and disabled).
import { useBraintreePayPalOneTimePaymentSession } from "@paypal/react-paypal-js/sdk-v6";
function CustomPayButton() {
const { handleClick, isPending, error } =
useBraintreePayPalOneTimePaymentSession({
amount: "50.00",
currency: "USD",
onApprove: async (data) => {
// tokenize and process
},
});
return (
<button onClick={handleClick} disabled={isPending}>
Pay with PayPal
</button>
);
}Returns: { handleClick: () => void, isPending: boolean, error: Error | null }
useBraintreePayPalBillingAgreementSession(props)
Creates a billing agreement session for vault flows. Returns a handleClick function to start the flow. Accepts the same props as BraintreePayPalBillingAgreementButton (minus type and disabled).
import { useBraintreePayPalBillingAgreementSession } from "@paypal/react-paypal-js/sdk-v6";
function CustomVaultButton() {
const { handleClick, isPending, error } =
useBraintreePayPalBillingAgreementSession({
billingAgreementDescription: "Monthly subscription",
planType: "SUBSCRIPTION",
onApprove: async (data) => {
// tokenize with data.billingToken
},
});
return (
<button onClick={handleClick} disabled={isPending}>
Save PayPal Account
</button>
);
}Returns: { handleClick: () => void, isPending: boolean, error: Error | null }
useBraintreePayPalCheckoutWithVaultSession(props)
Creates a checkout-with-vault session combining one-time payment and billing agreement consent. Returns a handleClick function. Accepts the same props as BraintreePayPalCheckoutWithVaultButton (minus type and disabled).
import { useBraintreePayPalCheckoutWithVaultSession } from "@paypal/react-paypal-js/sdk-v6";
function CustomCheckoutVaultButton() {
const { handleClick, isPending, error } =
useBraintreePayPalCheckoutWithVaultSession({
amount: "49.99",
currency: "USD",
billingAgreementDetails: { description: "Monthly subscription" },
onApprove: async (data) => {
// tokenize and process
},
});
return (
<button onClick={handleClick} disabled={isPending}>
Pay & Save
</button>
);
}Returns: { handleClick: () => void, isPending: boolean, error: Error | null }
useBraintreePayPalPayLaterSession(props)
Creates a Pay Later (Buy Now, Pay Later) session. Returns a handleClick function to start the flow. Accepts the same props as BraintreePayPalPayLaterButton (minus disabled) — use this hook directly when you need full control over the <paypal-pay-later-button> UI. Eligibility (countryCode/productCode) must still be fetched via useBraintreeEligibleMethods.
import {
useBraintreePayPalPayLaterSession,
useBraintreeEligibleMethods,
} from "@paypal/react-paypal-js/sdk-v6";
function CustomPayLaterButton() {
const { handleClick, isPending } = useBraintreePayPalPayLaterSession({
amount: "100.00",
currency: "USD",
onApprove: async (data) => {
// tokenize and process
},
});
const { isLoading, eligiblePaymentMethods } = useBraintreeEligibleMethods({
currency: "USD",
});
if (isPending || isLoading) {
return <Spinner />;
}
if (!eligiblePaymentMethods?.paylater) {
return null;
}
const payLaterDetails = eligiblePaymentMethods.getDetails("paylater");
return (
<paypal-pay-later-button
onClick={() => handleClick()}
disabled={isPending}
countryCode={payLaterDetails?.countryCode}
productCode={payLaterDetails?.productCode}
/>
);
}Returns: { handleClick: () => void, isPending: boolean, error: Error | null }
Key Braintree Types
Import these types from @paypal/react-paypal-js/sdk-v6:
import type {
BraintreeApprovalData,
BraintreeTokenizePayload,
BraintreeLineItem,
BraintreeShippingOption,
BraintreeAmountBreakdown,
BraintreePlanMetadata,
BraintreePresentationMode,
BraintreeV6Namespace,
} from "@paypal/react-paypal-js/sdk-v6";| Type | Description |
|---|---|
BraintreeApprovalData |
{ payerId?, orderId?, billingToken? } — data passed to onApprove |
BraintreeTokenizePayload |
{ nonce, type, details: { email, payerId, firstName, lastName, ... } } — result of tokenizePayment |
BraintreeLineItem |
{ quantity, unitAmount, name, kind, unitTaxAmount?, description? } — transaction line item |
BraintreeShippingOption |
{ id, label, selected, type, amount: { currency, value } } — shipping option |
BraintreeAmountBreakdown |
{ itemTotal?, shipping?, handling?, taxTotal?, insurance?, discount?, ... } — amount breakdown |
BraintreePlanMetadata |
{ billingCycles?, currencyIsoCode, name, ... } — subscription plan details |
BraintreePresentationMode |
"auto" | "popup" | "modal" | "redirect" | "payment-handler" | "direct-app-switch" |
BraintreeV6Namespace |
The Braintree namespace type expected by BraintreePayPalProvider |
Key Differences from Standard PayPal Buttons
- Different provider — uses
BraintreePayPalProviderwith a Braintree client token instead ofPayPalProviderwith a PayPal client ID - No
createOrder/orderId— payment sessions are created internally by the Braintree SDK; you passamountandcurrencydirectly as props - Nonce-based flow —
onApprovereceivesBraintreeApprovalData; callbraintreePayPalCheckoutInstance.tokenizePayment()to get a payment method nonce - Server-side with Braintree SDK — send the nonce to your server and process it with the Braintree server SDK (not PayPal's Orders API)
- Presentation modes — supports
"auto","popup","modal","redirect","payment-handler", and"direct-app-switch"
Payment Flow
- User clicks a payment button
handleClick()starts the payment sessioncreateOrdercallback creates an order via your backend API- PayPal opens the checkout experience (popup/modal/redirect)
- On approval,
onApprovecallback captures the order via the backend - Success/error handling displays the result to the user
Card Fields Components
Card Fields components provide customizable card input fields for collecting payment details directly on your page.
Requires "card-fields" in the provider's components array.
PayPalCardFieldsProvider
Wraps card field components and manages the Card Fields session.
import {
PayPalProvider,
PayPalCardFieldsProvider,
} from "@paypal/react-paypal-js/sdk-v6";
function App() {
return (
<PayPalProvider
clientToken="your-client-token"
environment="sandbox"
components={["card-fields"]}
pageType="checkout"
>
<CheckoutForm />
</PayPalProvider>
);
}
function CheckoutForm() {
return (
<PayPalCardFieldsProvider>
<CardPaymentForm />
</PayPalCardFieldsProvider>
);
}Props:
| Prop | Type | Required | Description |
|---|---|---|---|
amount |
OrderAmount |
No | Amount for the card transaction (e.g., { value: "10.00", currencyCode: "USD" }) |
isCobrandedEligible |
boolean |
No | Enable co-branded card eligibility |
blur |
(event) => void |
No | Callback when a field loses focus |
change |
(event) => void |
No | Callback when field value changes |
focus |
(event) => void |
No | Callback when field receives focus |
empty |
(event) => void |
No | Callback when field becomes empty |
notempty |
(event) => void |
No | Callback when field becomes non-empty |
validitychange |
(event) => void |
No | Callback when field validity changes |
cardtypechange |
(event) => void |
No | Callback when detected card type changes |
inputsubmit |
(event) => void |
No | Callback when submit key is pressed in field |
PayPalCardNumberField
Renders a card number input field. Must be used within a PayPalCardFieldsProvider component.
import { PayPalCardNumberField } from "@paypal/react-paypal-js/sdk-v6";
<PayPalCardNumberField
placeholder="Card number"
containerStyles={{ height: "3rem", marginBottom: "1rem" }}
/>;PayPalCardExpiryField
Renders a card expiry input field. Must be used within a PayPalCardFieldsProvider component.
import { PayPalCardExpiryField } from "@paypal/react-paypal-js/sdk-v6";
<PayPalCardExpiryField
placeholder="MM/YY"
containerStyles={{ height: "3rem", marginBottom: "1rem" }}
/>;PayPalCardCvvField
Renders a CVV input field. Must be used within a PayPalCardFieldsProvider component.
import { PayPalCardCvvField } from "@paypal/react-paypal-js/sdk-v6";
<PayPalCardCvvField
placeholder="CVV"
containerStyles={{ height: "3rem", marginBottom: "1rem" }}
/>;Field Component Props
All field components (PayPalCardNumberField, PayPalCardExpiryField, PayPalCardCvvField) accept the same set of props. They combine container styling properties with CardField-specific configuration options.
| Prop | Type | Required | Description |
|---|---|---|---|
placeholder |
string |
No | Placeholder text for the field |
label |
string |
No | Label text for the field |
style |
MerchantStyleObject |
No | Style object for the field |
ariaLabel |
string |
No | ARIA label for accessibility |
ariaDescription |
string |
No | ARIA description for accessibility |
ariaInvalidErrorMessage |
string |
No | ARIA error message when field is invalid |
containerStyles |
React.CSSProperties |
No | CSS styles for the field container wrapper |
containerClassName |
string |
No | CSS class name for the field container wrapper |
Payment Flow: Card Fields
- User enters card number, expiry, and CVV in the card fields
- User clicks your submit button
createOrdercreates an order via your backend APIsubmit(orderId)processes the card payment with the order IDsubmitResponseobject gets updated with the payment result- Handle submit response based on payment result
Hooks API
usePayPal
Returns the PayPal context including the SDK instance and loading status.
import {
usePayPal,
INSTANCE_LOADING_STATE,
} from "@paypal/react-paypal-js/sdk-v6";
function MyComponent() {
const {
sdkInstance, // The PayPal SDK instance
eligiblePaymentMethods, // Eligible payment methods
loadingStatus, // PENDING | RESOLVED | REJECTED
error, // Any initialization error
isHydrated, // SSR hydration status
} = usePayPal();
const isPending = loadingStatus === INSTANCE_LOADING_STATE.PENDING;
const isReady = loadingStatus === INSTANCE_LOADING_STATE.RESOLVED;
// ...
}useEligibleMethods
Returns eligible payment methods and loading state. Use this to conditionally render payment buttons based on eligibility. This hook also updates the PayPalProvider reducer with Eligibility Output from the SDK, enabling built-in eligibility features in the UI Button components.
import { useEligibleMethods } from "@paypal/react-paypal-js/sdk-v6";
function PaymentOptions() {
const { eligiblePaymentMethods, isLoading, error } = useEligibleMethods();
if (isLoading) {
return <div>Checking eligibility...</div>;
}
const isPayPalEligible = eligiblePaymentMethods?.isEligible("paypal");
const isVenmoEligible = eligiblePaymentMethods?.isEligible("venmo");
const isPayLaterEligible = eligiblePaymentMethods?.isEligible("paylater");
return (
<div>
{isPayPalEligible && <PayPalOneTimePaymentButton {...props} />}
{isVenmoEligible && <VenmoOneTimePaymentButton {...props} />}
{isPayLaterEligible && <PayLaterOneTimePaymentButton {...props} />}
</div>
);
}Stale Eligibility Data Prevention
When navigating between different payment flows (e.g., from a save payment page with paymentFlow: "VAULT_WITHOUT_PAYMENT" to a checkout page with paymentFlow: "ONE_TIME_PAYMENT"), isLoading will return true while the new eligibility data is being fetched. This prevents stale buttons from flashing before the updated eligibility response arrives.
If your app uses a single PayPalProvider across multiple routes with different paymentFlow values, always check isLoading before rendering eligibility-dependent buttons:
const { eligiblePaymentMethods, isLoading } = useEligibleMethods({
payload: { currencyCode: "USD", paymentFlow: "ONE_TIME_PAYMENT" },
});
// Guard eligibility-dependent buttons with isLoading to avoid rendering
// buttons based on stale data from a previous payment flow
const isPayLaterEligible =
!isLoading && eligiblePaymentMethods?.isEligible("paylater");usePayPalMessages
Hook for integrating PayPal messaging (Pay Later promotions).
import { usePayPalMessages } from "@paypal/react-paypal-js/sdk-v6";
function PayLaterMessage() {
const { error, isReady, handleFetchContent, handleCreateLearnMore } =
usePayPalMessages({
buyerCountry: "US",
currencyCode: "USD",
});
// Use to display financing messages
}usePayPalCardFields
Returns the Card Fields instance initialization errors. Must be used within a PayPalCardFieldsProvider component.
import { usePayPalCardFields } from "@paypal/react-paypal-js/sdk-v6";
function CardFields() {
const { error } = usePayPalCardFields();
useEffect(() => {
if (error) {
// Handle error logic
console.error("Error initializing PayPal Card Fields: ", error);
}
}, [error]);
return <CardPaymentForm />;
}Payment Session Hooks
For advanced use cases where you need full control over the payment flow, use the session hooks directly with web components.
Note: One-time payment session hooks (e.g.,
usePayPalOneTimePaymentSession) accept either a staticorderIdor acreateOrdercallback — they are mutually exclusive. UseorderIdwhen you've already created the order, orcreateOrderto defer order creation until the buyer clicks. The same pattern applies to save payment hooks withvaultSetupTokenvscreateVaultToken.
| Hook | Payment Type |
|---|---|
usePayPalOneTimePaymentSession |
PayPal |
useVenmoOneTimePaymentSession |
Venmo |
usePayLaterOneTimePaymentSession |
Pay Later |
usePayPalGuestPaymentSession |
Basic Card |
usePayPalSubscriptionPaymentSession |
Subscriptions |
usePayPalSavePaymentSession |
Save Payment Method |
usePayPalCreditOneTimePaymentSession |
Credit (One-time) |
usePayPalCreditSavePaymentSession |
Credit (Save) |
useGooglePayOneTimePaymentSession |
Google Pay |
useApplePayOneTimePaymentSession |
Apple Pay |
usePayPalOneTimePaymentSession
import { usePayPalOneTimePaymentSession } from "@paypal/react-paypal-js/sdk-v6";
function CustomPayPalButton() {
const { isPending, error, handleClick } = usePayPalOneTimePaymentSession({
createOrder: async () => {
const { orderId } = await createOrder();
return { orderId };
},
onApprove: (data: OnApproveDataOneTimePayments) =>
console.log("Approved:", data),
onCancel: (data: OnCancelDataOneTimePayments) => console.log("Cancelled"),
onError: (data: OnErrorData) => console.error(data),
onComplete: (data: OnCompleteData) =>
console.log("Payment session complete", data),
});
return (
<paypal-button
onClick={() => handleClick()}
type="pay"
disabled={isPending || error !== null}
/>
);
}useVenmoOneTimePaymentSession
import { useVenmoOneTimePaymentSession } from "@paypal/react-paypal-js/sdk-v6";
function CustomVenmoButton() {
const { handleClick } = useVenmoOneTimePaymentSession({
createOrder: async () => {
const { orderId } = await createOrder();
return { orderId };
},
onApprove: (data: OnApproveDataOneTimePayments) =>
console.log("Approved:", data),
onCancel: (data: OnCancelDataOneTimePayments) =>
console.log("Cancelled", data),
onError: (data: OnErrorData) => console.error("Error:", data),
onComplete: (data: OnCompleteData) =>
console.log("Payment session complete", data),
});
return <venmo-button onClick={() => handleClick()} />;
}usePayLaterOneTimePaymentSession
import {
usePayLaterOneTimePaymentSession,
useEligibleMethods,
} from "@paypal/react-paypal-js/sdk-v6";
function CustomPayLaterButton() {
const { handleClick } = usePayLaterOneTimePaymentSession({
createOrder: async () => {
const { orderId } = await createOrder();
return { orderId };
},
onApprove: (data: OnApproveDataOneTimePayments) =>
console.log("Approved:", data),
onCancel: (data: OnCancelDataOneTimePayments) =>
console.log("Cancelled", data),
onError: (data: OnErrorData) => console.error("Error:", data),
onComplete: (data: OnCompleteData) =>
console.log("Payment session complete", data),
});
// Fetch eligibility to obtain the countryCode/productCode the button needs
const { eligiblePaymentMethods, isLoading } = useEligibleMethods({
payload: { purchase_units: [{ amount: { currency_code: "USD" } }] },
});
if (isLoading) {
return null;
}
if (!eligiblePaymentMethods?.isEligible("paylater")) {
return null;
}
const payLaterDetails = eligiblePaymentMethods.getDetails("paylater");
return (
<paypal-pay-later-button
onClick={() => handleClick()}
countryCode={payLaterDetails?.countryCode}
productCode={payLaterDetails?.productCode}
/>
);
}usePayPalGuestPaymentSession
import { usePayPalGuestPaymentSession } from "@paypal/react-paypal-js/sdk-v6";
function CustomPayPalGuestButton() {
const { handleClick, buttonRef } = usePayPalGuestPaymentSession({
createOrder: async () => {
const { orderId } = await createOrder();
return { orderId };
},
onApprove: (data: OnApproveDataOneTimePayments) =>
console.log("Approved:", data),
onCancel: (data: OnCancelDataOneTimePayments) =>
console.log("Cancelled", data),
onError: (data: OnErrorData) => console.error("Error:", data),
onComplete: (data: OnCompleteData) =>
console.log("Payment session complete", data),
});
return (
<paypal-basic-card-container>
<paypal-basic-card-button ref={buttonRef} onClick={() => handleClick()} />
</paypal-basic-card-container>
);
}usePayPalSavePaymentSession
import { usePayPalSavePaymentSession } from "@paypal/react-paypal-js/sdk-v6";
function CustomPayPalSaveButton() {
const { handleClick } = usePayPalSavePaymentSession({
createVaultToken: async () => {
const { vaultSetupToken } = await createVaultToken();
return { vaultSetupToken };
},
onApprove: (data: OnApproveDataSavePayments) => console.log("Saved:", data),
onCancel: (data: OnCancelDataSavePayments) =>
console.log("Cancelled", data),
onError: (data: OnErrorData) => console.error("Error:", data),
onComplete: (data: OnCompleteData) =>
console.log("Payment session complete", data),
});
return <paypal-button onClick={() => handleClick()} type="pay" />;
}usePayPalSubscriptionPaymentSession
import { usePayPalSubscriptionPaymentSession } from "@paypal/react-paypal-js/sdk-v6";
function CustomPayPalSubscriptionButton() {
const { handleClick } = usePayPalSubscriptionPaymentSession({
createSubscription: async () => {
const response = await fetch("/api/create-subscription", {
method: "POST",
});
const { subscriptionId } = await response.json();
return { subscriptionId };
},
onApprove: (data: OnApproveDataOneTimePayments) =>
console.log("Subscription approved:", data),
onCancel: (data: OnCancelDataOneTimePayments) =>
console.log("Cancelled", data),
onError: (data: OnErrorData) => console.error("Error:", data),
onComplete: (data: OnCompleteData) =>
console.log("Payment session complete", data),
});
return <paypal-button onClick={() => handleClick()} type="subscribe" />;
}usePayPalCreditOneTimePaymentSession
For PayPal Credit one-time payments.
import {
usePayPalCreditOneTimePaymentSession,
useEligibleMethods,
} from "@paypal/react-paypal-js/sdk-v6";
function CustomPayPalCreditButton() {
const { handleClick } = usePayPalCreditOneTimePaymentSession({
createOrder: async () => {
const { orderId } = await createOrder();
return { orderId };
},
onApprove: (data: OnApproveDataOneTimePayments) =>
console.log("Credit approved:", data),
onCancel: (data: OnCancelDataOneTimePayments) =>
console.log("Cancelled", data),
onError: (data: OnErrorData) => console.error("Error:", data),
onComplete: (data: OnCompleteData) =>
console.log("Payment session complete", data),
});
// Fetch eligibility to obtain the countryCode the button needs
const { eligiblePaymentMethods, isLoading } = useEligibleMethods({
payload: { purchase_units: [{ amount: { currency_code: "USD" } }] },
});
if (isLoading) {
return null;
}
if (!eligiblePaymentMethods?.isEligible("credit")) {
return null;
}
const creditDetails = eligiblePaymentMethods.getDetails("credit");
return (
<paypal-credit-button
onClick={() => handleClick()}
countryCode={creditDetails?.countryCode}
/>
);
}usePayPalCreditSavePaymentSession
For saving PayPal Credit as a payment method.
import {
usePayPalCreditSavePaymentSession,
useEligibleMethods,
} from "@paypal/react-paypal-js/sdk-v6";
function CustomPayPalCreditSaveButton() {
const { handleClick } = usePayPalCreditSavePaymentSession({
createVaultToken: async () => {
const { vaultSetupToken } = await createVaultSetupToken();
return { vaultSetupToken };
},
onApprove: (data: OnApproveDataSavePayments) =>
console.log("Credit approved:", data),
onCancel: (data: OnCancelDataSavePayments) =>
console.log("Cancelled", data),
onError: (data: OnErrorData) => console.error("Error:", data),
onComplete: (data: OnCompleteData) =>
console.log("Payment session complete", data),
});
// Fetch eligibility to obtain the countryCode the button needs
const { eligiblePaymentMethods, isLoading } = useEligibleMethods({
payload: { purchase_units: [{ amount: { currency_code: "USD" } }] },
});
if (isLoading) {
return null;
}
if (!eligiblePaymentMethods?.isEligible("credit")) {
return null;
}
const creditDetails = eligiblePaymentMethods.getDetails("credit");
return (
<paypal-credit-button
onClick={() => handleClick()}
countryCode={creditDetails?.countryCode}
/>
);
}useApplePayOneTimePaymentSession
For advanced use cases where you need full control over the Apple Pay flow. Most integrations should use ApplePayOneTimePaymentButton instead.
import {
useApplePayOneTimePaymentSession,
useEligibleMethods,
} from "@paypal/react-paypal-js/sdk-v6";
function CustomApplePayButton() {
const { eligiblePaymentMethods, isLoading } = useEligibleMethods({
payload: { currencyCode: "USD" },
});
const applePayConfig =
eligiblePaymentMethods?.getDetails("applepay")?.config ?? null;
const { isPending, error, handleClick } = useApplePayOneTimePaymentSession({
applePayConfig,
paymentRequest: {
countryCode: "US",
currencyCode: "USD",
total: { label: "My Store", amount: "100.00", type: "final" },
},
applePaySessionVersion: 4,
createOrder: async () => {
const response = await fetch("/api/paypal/create-order", {
method: "POST",
});
const data = await response.json();
return { orderId: data.id };
},
onApprove: async (data) => {
const orderId = data.approveApplePayPayment.id;
await fetch(`/api/paypal/capture/${orderId}`, { method: "POST" });
},
onError: (err) => console.error("Apple Pay error:", err),
});
if (isLoading || !applePayConfig) {
return null;
}
if (error) {
return <div>Error: {error.message}</div>;
}
return (
<apple-pay-button
buttonstyle="black"
type="buy"
locale="en"
onClick={() => handleClick()}
disabled={isPending}
/>
);
}Card Fields Session Hooks
Use these hooks to handle the Card Fields payment flows. The hooks must be used within a PayPalCardFieldsProvider component.
| Hook | Payment Type |
|---|---|
usePayPalCardFieldsOneTimePaymentSession |
One-time |
usePayPalCardFieldsSavePaymentSession |
Save/Vault |
usePayPalCardFieldsOneTimePaymentSession
Hook for managing one-time payment Card Fields sessions.
import { usePayPalCardFieldsOneTimePaymentSession } from "@paypal/react-paypal-js/sdk-v6";
function CardPaymentForm() {
const { submit, submitResponse, error } =
usePayPalCardFieldsOneTimePaymentSession();
useEffect(() => {
if (error) {
// Handle submit error logic
console.error("Error submitting PayPal Card Fields payment", error);
}
}, [error]);
useEffect(() => {
if (!submitResponse) {
return;
}
const { orderId, message } = submitResponse.data;
switch (submitResponse.state) {
case "succeeded":
// Handle submit success logic
console.log(`One time payment succeeded: orderId: ${orderId}`);
break;
case "failed":
// Handle submit failed response logic
console.error(
`One time payment failed: orderId: ${orderId}, message: ${message}`,
);
break;
}
}, [submitResponse]);
const handleSubmit = async () => {
const orderId = await createOrder();
await submit(orderId);
};
return (
<div>
<PayPalCardNumberField />
<PayPalCardExpiryField />
<PayPalCardCvvField />
<button onClick={handleSubmit}>Pay</button>
</div>
);
}usePayPalCardFieldsSavePaymentSession
Hook for managing save payment Card Fields sessions.
import { usePayPalCardFieldsSavePaymentSession } from "@paypal/react-paypal-js/sdk-v6";
function CardPaymentForm() {
const { submit, submitResponse, error } =
usePayPalCardFieldsSavePaymentSession();
useEffect(() => {
if (error) {
// Handle submit error logic
console.error(
"Error submitting PayPal Card Fields payment method",
error,
);
}
}, [error]);
useEffect(() => {
if (!submitResponse) {
return;
}
const { vaultSetupToken, message } = submitResponse.data;
switch (submitResponse.state) {
case "succeeded":
// Handle submit success logic
console.log(
`Save payment method succeeded: vaultSetupToken: ${vaultSetupToken}`,
);
break;
case "failed":
// Handle submit failed response logic
console.error(
`Save payment method failed: vaultSetupToken: ${vaultSetupToken}, message: ${message}`,
);
break;
}
}, [submitResponse]);
const handleSubmit = async () => {
const { vaultSetupToken } = await createCardVaultToken();
await submit(vaultSetupToken);
};
return (
<div>
<PayPalCardNumberField />
<PayPalCardExpiryField />
<PayPalCardCvvField />
<button onClick={handleSubmit}>Save Payment Method</button>
</div>
);
}Web Components
The V6 SDK uses web components for rendering buttons. These are automatically typed when you import from @paypal/react-paypal-js/sdk-v6.
Available Web Components
| Component | Description |
|---|---|
<paypal-button> |
PayPal payment button |
<venmo-button> |
Venmo payment button |
<paypal-pay-later-button> |
Pay Later button |
<paypal-basic-card-container> |
Guest checkout container |
<paypal-basic-card-button> |
Guest checkout button |
<paypal-credit-button> |
PayPal Credit button |
<paypal-message> |
PayPal messaging component |
Button Types
The type prop controls the button label:
"pay"- "Pay with PayPal" (default)"checkout"- "Checkout with PayPal""buynow"- "Buy Now""donate"- "Donate""subscribe"- "Subscribe"
<paypal-button type="checkout" onClick={handleClick} />Server-Side Rendering
The useFetchEligibleMethods function is available from the server export path for pre-fetching eligibility data on the server. Pass the response to PayPalProvider via the eligibleMethodsResponse prop to avoid a client-side eligibility fetch.
// app/checkout/page.tsx (Next.js server component)
import { useFetchEligibleMethods } from "@paypal/react-paypal-js/sdk-v6/server";
import { PayPalProvider } from "@paypal/react-paypal-js/sdk-v6";
export default async function CheckoutPage() {
const eligibleMethodsResponse = await useFetchEligibleMethods({
environment: "sandbox",
headers: {
Authorization: `Bearer ${clientToken}`,
"Content-Type": "application/json",
},
payload: {
purchase_units: [{ amount: { currency_code: "USD", value: "100.00" } }],
},
});
return (
<PayPalProvider
clientId={clientId}
environment="sandbox"
pageType="checkout"
eligibleMethodsResponse={eligibleMethodsResponse}
>
<CheckoutForm />
</PayPalProvider>
);
}Migration from v8.x (Legacy SDK)
The v9.0.0 release introduces the V6 SDK with a new API. Here are the key differences:
| v8.x (Legacy) | v9.0.0 (V6 SDK) |
|---|---|
PayPalScriptProvider |
PayPalProvider |
PayPalButtons |
PayPalOneTimePaymentButton or hooks |
options={{ clientId }} |
clientId={clientId} or clientToken={clientToken} |
createOrder returns orderId |
createOrder returns { orderId } |
@paypal/react-paypal-js |
@paypal/react-paypal-js/sdk-v6 |
Before (v8.x)
import { PayPalScriptProvider, PayPalButtons } from "@paypal/react-paypal-js";
<PayPalScriptProvider options={{ clientId: "test" }}>
<PayPalButtons
createOrder={() => {
return fetch("/api/orders", { method: "POST" })
.then((res) => res.json())
.then((order) => order.id);
}}
onApprove={(data) => {
return fetch(`/api/orders/${data.orderID}/capture`, {
method: "POST",
});
}}
/>
</PayPalScriptProvider>;After (v9.0.0)
import {
PayPalProvider,
PayPalOneTimePaymentButton,
} from "@paypal/react-paypal-js/sdk-v6";
<PayPalProvider clientId={clientId} environment="sandbox" pageType="checkout">
<PayPalOneTimePaymentButton
createOrder={async () => {
const res = await fetch("/api/orders", { method: "POST" });
const order = await res.json();
return { orderId: order.id };
}}
onApprove={async ({ orderId }) => {
await fetch(`/api/orders/${orderId}/capture`, { method: "POST" });
}}
/>
</PayPalProvider>;For the legacy API documentation, see README-v8.md.
TypeScript
This package includes full TypeScript definitions. Import types from the same path:
import type {
// Web component props
ButtonProps,
PayLaterButtonProps,
PayPalBasicCardButtonProps,
PayPalCreditButtonProps,
// Session hook props
UsePayPalOneTimePaymentSessionProps,
UseVenmoOneTimePaymentSessionProps,
UsePayLaterOneTimePaymentSessionProps,
UsePayPalGuestPaymentSessionProps,
UsePayPalSubscriptionPaymentSessionProps,
UsePayPalSavePaymentSessionProps,
UsePayPalCreditOneTimePaymentSessionProps,
UsePayPalCreditSavePaymentSessionProps,
// Button component props
PayPalSubscriptionButtonProps,
PayPalCreditOneTimePaymentButtonProps,
PayPalCreditSavePaymentButtonProps,
// Enums
INSTANCE_LOADING_STATE,
} from "@paypal/react-paypal-js/sdk-v6";Web Component Types
The package automatically extends JSX types to include PayPal web components. No additional configuration is needed for React 17, 18, or 19.
Browser Support
This library supports all modern browsers. See the PayPal browser support documentation for the full list.