# Set up future PayPal payments Learn how to save PayPal details and charge your customers later. Set up future PayPal payments to save customer payment details for subscriptions, delayed charges, and streamlined future purchases. Learn how to enable and use recurring payments with PayPal through Stripe. ## Enable recurring payments Stripe automatically enables recurring payments for most users when they [activate PayPal payments](https://docs.stripe.com/payments/paypal/activate.md) in the Stripe Dashboard. However, due to PayPal’s policies and regional restrictions, some users might need to enable recurring payments manually. This includes users that set up their accounts before we introduced automatic enablement. To manually enable recurring payments: 1. Go to your [Payment methods settings](https://dashboard.stripe.com/settings/payment_methods). 1. Click **PayPal** > **Enable** in the **Recurring payments** section. After you enable recurring payments, it appears as **pending** in the Dashboard. It usually takes up to five business days to get access. When you’re granted access, recurring payments are available in your [PayPal settings](https://dashboard.stripe.com/settings/payment_methods). In testing environments, recurring payments are enabled by default. # Direct API You can use [Setup Intents](https://docs.stripe.com/api/setup_intents.md) to collect PayPal payment method details in advance, and determine the final amount or payment date later. Use it to: - Save payment methods to a wallet to streamline future purchases - Collect surcharges after fulfilling a service - [Start a free trial for a subscription](https://docs.stripe.com/billing/subscriptions/trials.md) ## Set up Stripe [Server-side] First, you need a Stripe account. [Register now](https://dashboard.stripe.com/register). Use our official libraries for access to the Stripe API from your application: #### Ruby ```bash # Available as a gem sudo gem install stripe ``` ```ruby # If you use bundler, you can add this line to your Gemfile gem 'stripe' ``` ## Create or retrieve a customer before setup [Server-side] To reuse a PayPal payment method for future payments, attach it to an object that represents your customer. > #### Use the Accounts v2 API to represent customers > > The Accounts v2 API is generally available for Connect users, and in public preview for other Stripe users. If you’re part of the Accounts v2 preview, you need to specify a [specify a preview version](https://docs.stripe.com/api-v2-overview.md#sdk-and-api-versioning) in your code. > > To request access to the Accounts v2 preview, > > For most use cases, we recommend [modeling your customers as customer-configured Account objects](https://docs.stripe.com/accounts-v2/use-accounts-as-customers.md) instead of using [Customer](https://docs.stripe.com/api/customers.md) objects. Create a customer-configured [Account](https://docs.stripe.com/api/v2/core/accounts/create.md#v2_create_accounts-configuration-customer) or [Customer](https://docs.stripe.com/api/customers/create.md) when your customer creates an account with your business, or when saving a payment method. Associate the object’s ID with your own internal representation of a customer. Create a new customer or retrieve an existing one to associate with this payment. #### Accounts v2 ```curl curl -X POST https://api.stripe.com/v2/core/accounts \ -H "Authorization: Bearer <>" \ -H "Stripe-Version: 2026-05-27.preview" \ --json '{ "contact_email": "jenny.rosen@example.com", "display_name": "Jenny Rosen", "configuration": { "customer": {} }, "include": [ "configuration.customer" ] }' ``` #### Customers v1 ```curl curl https://api.stripe.com/v1/customers \ -u "<>:" \ -d "name=Jenny Rosen" \ --data-urlencode "email=jenny.rosen@example.com" ``` ## Create a SetupIntent [Server-side] A [SetupIntent](https://docs.stripe.com/api/setup_intents.md) is an object that represents your intent to set up a customer’s payment method for future payments. The `SetupIntent` tracks the steps of this setup process. Create a `SetupIntent` on your server. Include `paypal` in the [payment_method_types](https://docs.stripe.com/api/setup_intents/create.md#create_setup_intent-payment_method_types) array and specify the ID of the customer-configured [Account](https://docs.stripe.com/api/v2/core/accounts.md) or [Customer](https://docs.stripe.com/api/customers.md). #### Accounts v2 ```curl curl https://api.stripe.com/v1/setup_intents \ -u "<>:" \ -d "customer_account={{CUSTOMERACCOUNT_ID}}" \ -d "payment_method_types[]=paypal" \ -d "payment_method_data[type]=paypal" ``` #### Customers v1 ```curl curl https://api.stripe.com/v1/setup_intents \ -u "<>:" \ -d "customer={{CUSTOMER_ID}}" \ -d "payment_method_types[]=paypal" \ -d "payment_method_data[type]=paypal" ``` The `SetupIntent` object contains a [client_secret](https://docs.stripe.com/api/setup_intents/object.md#setup_intent_object-client_secret), a unique key that you need to pass to Stripe on the client side to redirect your buyer to PayPal and authorize the mandate. ## Redirect your customer [Client-side] When a customer attempts to set up their PayPal account for future payments, we recommend you use [Stripe.js](https://docs.stripe.com/js.md) to confirm the SetupIntent. Stripe.js is our foundational JavaScript library for building payment flows. It will automatically handle complexities like the redirect described below, and enables you to easily extend your integration to other payment methods in the future. Include the Stripe.js script on your checkout page by adding it to the head of your HTML file. ```html Checkout ``` Create an instance of Stripe.js with the following JavaScript on your checkout page. ```javascript // Set your publishable key. Remember to change this to your live publishable key in production! // See your keys here: https://dashboard.stripe.com/apikeys const stripe = Stripe('<>', {} ); ``` To confirm the setup on the client side, pass the client secret of the SetupIntent object that you created in Step 3. The client secret is different from your API keys that authenticate Stripe API requests. It should still be handled carefully because it can complete the charge. Don’t log it, embed it in URLs, or expose it to anyone but the customer. ### Confirm PayPal Setup To authorize you to use their PayPal account for future payments, your customer will be redirected to a PayPal billing agreement page, which they will need to approve before being redirected back to your website. Use [stripe.confirmPayPalSetup](https://docs.stripe.com/js/setup_intents/confirm_paypal_setup) to handle the redirect away from your page and to complete the setup. Add a `return_url` to this function to indicate where Stripe should redirect the user to after they approve the billing agreement on PayPal’s website. ```javascript // Redirects away from the client const {error} = await stripe.confirmPayPalSetup( '{{SETUP_INTENT_CLIENT_SECRET}}', { return_url: 'https://example.com/setup/complete', mandate_data: { customer_acceptance: { type: 'online', online: { infer_from_client: true } } }, } ); if (error) { // Inform the customer that there was an error. } ``` You can find the Payment Method payer ID and Billing Agreement ID on the resulting [Mandate](https://docs.stripe.com/api/mandates/.md) under the [payment_method_details](https://docs.stripe.com/api/mandates/object.md#mandate_object-payment_method_details-paypal) property. You can also find the buyer’s email and payer ID in the [paypal](https://docs.stripe.com/api/payment_methods/object.md#payment_method_object-paypal) property on the [PaymentMethod](https://docs.stripe.com/api/payment_methods.md). | Field | Value | | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | | `payer_email` | The email address of the payer on their PayPal account. | | `payer_id` | A unique ID of the payer’s PayPal account. | | `billing_agreement_id` | The PayPal Billing Agreement ID (BAID). This is an ID generated by PayPal which represents the mandate between the business and the customer. | ## Monitor webhooks [Server-side] Use a method such as [webhooks](https://docs.stripe.com/payments/payment-intents/verifying-status.md#webhooks) to confirm the billing agreement was authorized successfully by your customer, instead of relying on your customer to return to the payment status page. When a customer successfully authorizes the billing agreement, the SetupIntent emits the [setup_intent.succeeded](https://docs.stripe.com/api/events/types.md#event_types-setup_intent.succeeded) *webhook* (A webhook is a real-time push notification sent to your application as a JSON payload through HTTPS requests) event. If a customer doesn’t successfully authorize the billing agreement, the SetupIntent will emit the [setup_intent.setup_failed](https://docs.stripe.com/api/events/types.md#event_types-setup_intent.setup_failed) webhook event and returns to a status of `requires_payment_method`. When a customer revokes the billing agreement from their PayPal account, the [mandate.updated](https://docs.stripe.com/api/events/types.md#event_types-mandate.updated) is emitted. ## Charge off-session payments with a saved PayPal payment method [Server-side] When you’re ready to charge your customer off-session, use the customer and *PaymentMethod* (PaymentMethods represent your customer's payment instruments, used with the Payment Intents or Setup Intents APIs) IDs to create a [PaymentIntent](https://docs.stripe.com/api/payment_intents.md). To find a `paypal` instrument to charge, [list](https://docs.stripe.com/api/payment_methods/list.md) the PaymentMethods associated with your customer. #### Accounts v2 ```curl curl -G https://api.stripe.com/v1/payment_methods \ -u "<>:" \ -d "customer_account={{CUSTOMERACCOUNT_ID}}" \ -d type=paypal ``` #### Customers v1 ```curl curl -G https://api.stripe.com/v1/payment_methods \ -u "<>:" \ -d "customer={{CUSTOMER_ID}}" \ -d type=paypal ``` When you have the customer and `PaymentMethod` IDs, create a `PaymentIntent` with the amount and currency of the payment. Set the following parameters to make the *off-session payment* (A payment is described as off-session if it occurs without the direct involvement of the customer, using previously-collected payment information): - Set [off_session](https://docs.stripe.com/api/payment_intents/confirm.md#confirm_payment_intent-off_session) to `true` to indicate that the customer isn’t in your checkout flow during this payment attempt. This causes the PaymentIntent to throw an error if authentication is required. - Set the value of the PaymentIntent’s [confirm](https://docs.stripe.com/api/payment_intents/create.md#create_payment_intent-confirm) property to `true`, which causes confirmation to occur immediately when the PaymentIntent is created. - Set [payment_method](https://docs.stripe.com/api.md#create_payment_intent-payment_method) to the ID of the `PaymentMethod`. - Set [customer](https://docs.stripe.com/api.md#create_payment_intent-customer) or [customer_account](https://docs.stripe.com/api.md#create_payment_intent-customer_account) to the customer ID. #### Accounts v2 ```curl curl https://api.stripe.com/v1/payment_intents \ -u "<>:" \ -d amount=1099 \ -d currency=eur \ -d "customer_account={{CUSTOMERACCOUNT_ID}}" \ -d "payment_method_types[]=paypal" \ -d "payment_method={{PAYMENTMETHOD_ID}}" \ -d off_session=true \ -d confirm=true ``` #### Customers v1 ```curl curl https://api.stripe.com/v1/payment_intents \ -u "<>:" \ -d amount=1099 \ -d currency=eur \ -d "customer={{CUSTOMER_ID}}" \ -d "payment_method_types[]=paypal" \ -d payment_method={{PAYMENT_METHOD_ID}} \ -d off_session=true \ -d confirm=true ``` ## Charge on-session payments with a saved PayPal payment method [Client-side] When you’re ready to charge your customer on-session, use the *Customer* (Customer objects represent customers of your business. They let you reuse payment methods and give you the ability to track multiple payments) and *PaymentMethod* (PaymentMethods represent your customer's payment instruments, used with the Payment Intents or Setup Intents APIs) IDs to create a [PaymentIntent](https://docs.stripe.com/api/payment_intents.md). To find a `paypal` instrument to charge, [list the PaymentMethods](https://docs.stripe.com/api/payment_methods/list.md) associated with your Customer. #### Accounts v2 ```curl curl -G https://api.stripe.com/v1/payment_methods \ -u "<>:" \ -d "customer_account={{CUSTOMERACCOUNT_ID}}" \ -d type=paypal ``` #### Customers v1 ```curl curl -G https://api.stripe.com/v1/payment_methods \ -u "<>:" \ -d "customer={{CUSTOMER_ID}}" \ -d type=paypal ``` When you have the Customer and PaymentMethod IDs, create a PaymentIntent with the amount and currency of the payment: ```curl curl https://api.stripe.com/v1/payment_intents \ -u "<>:" \ -d amount=1099 \ -d currency=eur \ -d customer={{CUSTOMER_ID}} \ -d "payment_method_types[]=paypal" \ -d payment_method={{PAYMENT_METHOD_ID}} ``` Using Stripe.js SDK, call the [confirmPayPalPayment](https://docs.stripe.com/js/payment_intents/confirm_paypal_payment) function to execute the created PaymentIntent: ```javascript // Confirms the on-session payment const {error} = await stripe.confirmPayPalPayment( '{{PAYMENT_INTENT_CLIENT_SECRET}}', {payment_method: '{{PAYMENT_METHOD_ID}}'} // Note: return_url is not required here because the PayPal payment method was // previously set up using either a SetupIntent or a PaymentIntent with setup_future_usage ); if (error) { // Inform the customer that there was an error. } ``` > **Note**: The `return_url` parameter is conditionally required for `confirmPayPalPayment`: > > - It’s *not required* when using a PayPal payment method that was previously set up with a SetupIntent or a PaymentIntent with `setup_future_usage`. - It’s *required* for all other cases, including when creating a new PayPal payment method on-session. ## User-initiated payment method cancellation [Server-side] A customer can cancel the subscription (Billing Agreement) through their PayPal account. When they do so, Stripe emits a [mandate.updated](https://docs.stripe.com/api/events/types.md#event_types-mandate.updated) webhook. All subsequent PaymentIntents using the saved Payment Method will fail until you change to a Payment Method with active mandates. When payments fail for Subscriptions, the status changes to the Subscription status configured in your [automatic collection settings](https://docs.stripe.com/invoicing/automatic-collection.md). Notify the customer of failure and [charge them with a different payment method](https://docs.stripe.com/billing/subscriptions/overview.md#requires-payment-method). ## Optional: Set up future PayPal payments and capture a payment [Server-side] It’s also possible to set up a PayPal Payment Method for future usage and also charge at the same time when creating a [PaymentIntent](https://docs.stripe.com/api/payment_intents.md). Set [setup_future_usage](https://docs.stripe.com/api/payment_intents/create.md#create_payment_intent-setup_future_usage) to `off_session` to indicate that you want to set up the payment method for future usage. ```curl curl https://api.stripe.com/v1/payment_intents \ -u "<>:" \ -d amount=1099 \ -d currency=eur \ -d "payment_method_types[]=paypal" \ -d setup_future_usage=off_session ``` ## Optional: Handle the PayPal redirect manually [Server-side] We recommend relying on Stripe.js to handle PayPal redirects and billing authorizations client-side with `confirmPayPalSetup`. Using Stripe.js makes it much easier to extend your integration to other payment methods. However, you can also manually redirect your customers on your server by following these steps: You can *confirm* (Confirming an intent indicates that the customer intends to use the current or provided payment method. Upon confirmation, the intent attempts to initiate the portions of the flow that have real-world side effects) the SetupIntent at creation time by setting `confirm: true` and providing data about the mandate in the [mandate_data](https://docs.stripe.com/api/setup_intents/create.md#create_setup_intent-mandate_data) parameter. A `return_url` must be provided when confirming a SetupIntent to indicate where Stripe should redirect the user to after they complete the set-up on PayPal’s website or mobile application. ```curl curl https://api.stripe.com/v1/setup_intents \ -u "<>:" \ -d "customer={{CUSTOMER_ID}}" \ -d "payment_method_types[]=paypal" \ -d "payment_method_data[type]=paypal" \ -d usage=off_session \ -d "mandate_data[customer_acceptance][type]=online" \ -d "mandate_data[customer_acceptance][online][ip_address]={{IP_ADDRESS}}" \ -d "mandate_data[customer_acceptance][online][user_agent]={{USER_AGENT}}" \ -d confirm=true \ --data-urlencode "return_url=https://example.com/setup/complete" ``` Check that the SetupIntent has a status of `requires_action` and the type for `next_action` is `redirect_to_url`. ```json { "id": "seti_1IQ9hjJJahOk1vSNevPWnhEN", "object": "setup_intent","status": "requires_action", "next_action": { "type": "redirect_to_url", "redirect_to_url": { "url": "https://hooks.stripe.com/...", "return_url": "https://example.com/setup/complete" } }, "application": null, "cancellation_reason": null, "client_secret": "seti_1IQ9hjJJahOk1vSNevPWnhEN_secret_J2EAlI0GQbQKV9tg7ITRcUWRBiAwvUV", "created": 1614597263, "customer": null, "description": null, "last_setup_error": null, "latest_attempt": "setatt_1IQ9hkJJahOk1vSN0rsCpnLI", "livemode": false, "mandate": null, "metadata": {}, "next_action": null, "on_behalf_of": null, "payment_method": "pm_1IQ9hjJJahOk1vSNDc5lQWia", "payment_method_options": {}, "payment_method_types": ["paypal"], "single_use_mandate": null, "usage": "off_session" } ``` Redirect the customer to the URL provided in the `next_action.redirect_to_url.url` property. The code example here is approximate—the redirect method might be different in your web framework. #### Ruby ```ruby if setup_intent.status == 'requires_action' && setup_intent.next_action.type == 'redirect_to_url' url = setup_intent.next_action.redirect_to_url.url redirect(url) end ``` When the customer finishes the authorization process, they’re sent to the `return_url` configured in step 1. The `setup_intent` and `setup_intent_client_secret` URL query parameters are included and you can pass through your own query parameters, as described above. ## Optional: Handle risk library integration for on-session payments manually [Server-side] We recommend relying on Stripe.js to handle on-session payments with a saved PayPal payment method because it comes with a built-in [Fraudnet](https://developer.paypal.com/limited-release/fraudnet/) integration. However, you can also manually confirm PayPal PaymentIntents on your server by following these steps: Integrate with PayPal’s risk libraries ([Fraudnet](https://developer.paypal.com/limited-release/fraudnet/) for web and [Magnes](https://developer.paypal.com/limited-release/magnes/) for mobile) to allow PayPal to collect risk data when the buyer is present in the payment session. This reduces fraud and can increase your payments conversion for on-session payments. You need the Client Metadata ID (also known as Risk Correlation ID) used to initialize the library when making the API call to Stripe. After the library loads, you can create a PaymentIntent with the Client Metadata ID, amount, and currency of the payment: ```curl curl https://api.stripe.com/v1/payment_intents \ -u "<>:" \ -d amount=1099 \ -d currency=eur \ -d customer={{CUSTOMER_ID}} \ -d "payment_method_types[]=paypal" \ -d "payment_method_options[paypal][risk_correlation_id]={{RISK_CORRELATION_ID}}" \ -d confirm=true \ -d payment_method={{PAYMENT_METHOD_ID}} ``` You’ll get a `paypal_risk_correlation_id_missing` message code if you fail to pass the [risk_correlation_id](https://docs.stripe.com/api/payment_intents/confirm.md#confirm_payment_intent-payment_method_options-paypal-risk_correlation_id) parameter when confirming an on-session payment. ## Optional: Remove a saved PayPal account [Server-side] You can use the [detach](https://docs.stripe.com/api/payment_methods/detach.md) API to remove a customer’s saved PayPal account as a payment method. When you detach a PayPal payment method, it revokes the [mandate](https://docs.stripe.com/api/mandates.md) and also calls the PayPal API to cancel the associated PayPal billing agreement. ```curl curl -X POST https://api.stripe.com/v1/payment_methods/{{PAYMENT_METHOD_ID}}/detach \ -u "<>:" ```