PayPal has no native Bolt integration (unlike Stripe, which is first-class). To add PayPal payments, create a PayPal developer app for client ID and secret, build a Next.js API route for server-side token exchange and order creation, and use the @paypal/paypal-js SDK for the client-side checkout button. PayPal webhooks require a deployed URL — test order creation in the preview but complete end-to-end testing only after deploying to Netlify or Bolt Cloud.
Adding PayPal Payments to Bolt.new — The Manual Approach
PayPal is used by 435 million account holders worldwide and is often requested by customers who don't want to enter card details. However, Bolt.new's native payment integration is exclusive to Stripe — there is no PayPal connector in Bolt's settings panel. Implementing PayPal requires manually building both the server-side API logic and the client-side checkout UI, which takes significantly more effort than Stripe's one-prompt setup.
PayPal's payment flow has two server-side steps that must happen before the customer sees the payment interface: first, your server acquires an OAuth access token using your client ID and secret, then uses that token to create an order with the amount and currency. These steps require your PayPal secret key and must happen server-side to prevent credential exposure. A Next.js API route handles both steps, returning the PayPal order ID to the client. The client-side @paypal/paypal-js SDK then renders the familiar PayPal button and smart payment panel (PayPal balance, Venmo, credit/debit card) using that order ID.
A critical limitation to understand upfront: PayPal webhooks (the order.approved and payment.capture.completed events that confirm payment on your server) require a publicly accessible URL. Bolt's WebContainer runtime has no public URL — it runs inside a browser tab. This means you can test the order creation flow in the Bolt preview, but you cannot test the complete end-to-end payment confirmation until you deploy to Netlify or Bolt Cloud. Plan your development workflow around this: build and test the UI first, then complete end-to-end testing after your first deployment.
Integration method
PayPal integration in Bolt requires a manual two-part implementation: a server-side Next.js API route for OAuth token acquisition and order creation (keeping client ID and secret secure), and the @paypal/paypal-js SDK on the client for rendering the PayPal button and checkout UI. Unlike Stripe's one-prompt setup in Bolt, PayPal requires building each piece manually. PayPal webhooks cannot be received in Bolt's WebContainer — you must deploy to a real server before testing end-to-end payment flows.
Prerequisites
- A PayPal developer account at developer.paypal.com (free, separate from your personal PayPal)
- A PayPal app created in the developer dashboard with sandbox client ID and secret
- Two PayPal sandbox test accounts (buyer and seller) created in the developer dashboard
- A Bolt.new project using Next.js for API routes that will handle server-side PayPal logic
- Understanding that full end-to-end payment testing requires deploying to Netlify or Bolt Cloud first
Step-by-step guide
Create a PayPal Developer App and Get Credentials
Create a PayPal Developer App and Get Credentials
PayPal uses a separate developer environment at developer.paypal.com that's completely isolated from your personal or business PayPal account. You need to create a developer app to get the sandbox credentials used for testing. Log in at developer.paypal.com using your PayPal credentials. Navigate to My Apps & Credentials. Click 'Create App' under the Sandbox section. Give it a descriptive name like 'MyBoltApp-Sandbox.' After creation, you'll see your Sandbox Client ID (a long string starting with 'A...') and a Secret (click to reveal). Copy both. Also on the developer dashboard, you'll see pre-created sandbox test accounts — you need a sandbox buyer account to test the payment flow. Find the sandbox personal account email and password under Sandbox → Accounts. When you're ready for production, create a separate Live app and use those credentials instead. Never mix sandbox and live credentials.
Pro tip: The sandbox client ID is safe to expose to the browser (it's used to initialize the PayPal JS SDK on the client side). Only the sandbox secret must be kept server-side.
Expected result: You have your PayPal Sandbox Client ID and Sandbox Secret. You have the email and password for a sandbox buyer account for testing. All three are saved somewhere safe.
Install PayPal Dependencies and Configure Environment Variables
Install PayPal Dependencies and Configure Environment Variables
You'll need two PayPal packages: @paypal/paypal-js (or @paypal/react-paypal-js for React integration) for the client-side payment button UI, and no additional package for server-side API calls since you'll use the native fetch API for PayPal's REST API. The @paypal/react-paypal-js package provides the PayPalScriptProvider context and PayPalButtons component, which handle the complex PayPal SDK lifecycle management automatically. Store your credentials in .env — the client ID uses NEXT_PUBLIC_ prefix because it's needed by the client-side PayPal button script, but the secret uses no prefix because it's only read by your API routes. This is one of the rare cases where a 'public' variable is intentionally exposed — PayPal client IDs are designed to be visible in client code.
Install @paypal/react-paypal-js. Create a .env file with NEXT_PUBLIC_PAYPAL_CLIENT_ID (my sandbox client ID) and PAYPAL_CLIENT_SECRET (my sandbox secret). Create a .env.example file documenting these variables. Also create a .env.production.example with placeholder values for the live PayPal credentials.
Paste this in Bolt.new chat
1# .env — sandbox for development2NEXT_PUBLIC_PAYPAL_CLIENT_ID=your-sandbox-client-id-here3PAYPAL_CLIENT_SECRET=your-sandbox-secret-here4PAYPAL_API_URL=https://api-m.sandbox.paypal.com56# For production, change to:7# NEXT_PUBLIC_PAYPAL_CLIENT_ID=your-live-client-id8# PAYPAL_CLIENT_SECRET=your-live-secret9# PAYPAL_API_URL=https://api-m.paypal.comPro tip: The PAYPAL_API_URL environment variable makes switching between sandbox and production as simple as changing one URL — no code changes needed.
Expected result: @paypal/react-paypal-js is in package.json. The .env file has your sandbox client ID and secret. The dev server restarts successfully.
Build the Server-Side API Routes
Build the Server-Side API Routes
PayPal's server-side flow has two distinct steps that each require a separate API route. The first route creates a PayPal order: it acquires an OAuth access token using your client ID and secret (a credentials-based POST request), then uses that token to create an order with the amount, currency, and intent. The route returns the PayPal order ID to the client. The second route captures the order after the customer approves it in the PayPal interface: the client sends the order ID back to this route, which makes a PayPal API call to capture the funds. The capture response contains the transaction details (amount, payer info, transaction ID) that you should save to your database. Both routes require the PAYPAL_CLIENT_SECRET which never leaves the server.
Create two Next.js API routes for PayPal. First, /api/paypal/create-order (POST): accepts an amount and currency in the request body. Gets a PayPal OAuth token by POSTing to PAYPAL_API_URL/v1/oauth2/token with client credentials. Then creates a PayPal order at PAYPAL_API_URL/v2/checkout/orders with CAPTURE intent. Returns the order ID. Second, /api/paypal/capture-order (POST): accepts an orderID. Calls the PayPal capture endpoint at PAYPAL_API_URL/v2/checkout/orders/{orderID}/capture. Returns the capture response. Include error handling for failed auth and failed captures.
Paste this in Bolt.new chat
1// app/api/paypal/create-order/route.ts2import { NextResponse } from 'next/server';34async function getPayPalAccessToken(): Promise<string> {5 const auth = Buffer.from(6 `${process.env.NEXT_PUBLIC_PAYPAL_CLIENT_ID}:${process.env.PAYPAL_CLIENT_SECRET}`7 ).toString('base64');89 const response = await fetch(10 `${process.env.PAYPAL_API_URL}/v1/oauth2/token`,11 {12 method: 'POST',13 headers: {14 'Authorization': `Basic ${auth}`,15 'Content-Type': 'application/x-www-form-urlencoded',16 },17 body: 'grant_type=client_credentials',18 }19 );20 const data = await response.json();21 return data.access_token;22}2324export async function POST(request: Request) {25 try {26 const { amount, currency = 'USD' } = await request.json();27 const accessToken = await getPayPalAccessToken();2829 const response = await fetch(30 `${process.env.PAYPAL_API_URL}/v2/checkout/orders`,31 {32 method: 'POST',33 headers: {34 'Authorization': `Bearer ${accessToken}`,35 'Content-Type': 'application/json',36 },37 body: JSON.stringify({38 intent: 'CAPTURE',39 purchase_units: [{40 amount: {41 currency_code: currency,42 value: amount.toFixed(2),43 },44 }],45 }),46 }47 );4849 const order = await response.json();50 return NextResponse.json({ id: order.id });51 } catch (error) {52 return NextResponse.json({ error: 'Failed to create order' }, { status: 500 });53 }54}Pro tip: Cache the PayPal access token (it's valid for 9 hours) rather than requesting a new one on every order creation. Store it in memory with an expiry timestamp to avoid unnecessary OAuth calls.
Expected result: POST to /api/paypal/create-order with {amount: 29.99} returns a JSON object with an order id starting with a PayPal order ID format. The /api/paypal/capture-order route is also created and ready.
Add the PayPal Button Component to Your UI
Add the PayPal Button Component to Your UI
The @paypal/react-paypal-js library handles the complex PayPal JavaScript SDK lifecycle — loading the external PayPal script, initializing it with your client ID, and rendering the Smart Payment Button. You wrap your payment page with the PayPalScriptProvider context component (providing your client ID from the NEXT_PUBLIC_ environment variable), then use the PayPalButtons component where you want the button to appear. The PayPalButtons component takes two key callback props: createOrder (which calls your /api/paypal/create-order route and returns the order ID) and onApprove (which calls your /api/paypal/capture-order route after the customer approves payment in the PayPal popup). The PayPal button handles all the popup/modal logic internally — you just need to handle the success and error callbacks.
Create a PayPalCheckout component that uses PayPalScriptProvider from @paypal/react-paypal-js to wrap PayPalButtons. The component accepts an amount prop (number) and an onSuccess callback prop. The createOrder function should POST to /api/paypal/create-order with the amount. The onApprove function should POST the orderID to /api/paypal/capture-order and call onSuccess with the capture details on completion. Handle errors with a visible error message. Use NEXT_PUBLIC_PAYPAL_CLIENT_ID as the client ID.
Paste this in Bolt.new chat
1'use client';2import { PayPalScriptProvider, PayPalButtons } from '@paypal/react-paypal-js';3import { useState } from 'react';45interface PayPalCheckoutProps {6 amount: number;7 currency?: string;8 onSuccess?: (details: Record<string, unknown>) => void;9}1011export function PayPalCheckout({12 amount,13 currency = 'USD',14 onSuccess,15}: PayPalCheckoutProps) {16 const [error, setError] = useState<string | null>(null);1718 return (19 <PayPalScriptProvider20 options={{21 clientId: process.env.NEXT_PUBLIC_PAYPAL_CLIENT_ID!,22 currency,23 }}24 >25 <div className="max-w-sm">26 <PayPalButtons27 createOrder={async () => {28 setError(null);29 const response = await fetch('/api/paypal/create-order', {30 method: 'POST',31 headers: { 'Content-Type': 'application/json' },32 body: JSON.stringify({ amount, currency }),33 });34 const order = await response.json();35 if (!response.ok) throw new Error(order.error);36 return order.id;37 }}38 onApprove={async (data) => {39 const response = await fetch('/api/paypal/capture-order', {40 method: 'POST',41 headers: { 'Content-Type': 'application/json' },42 body: JSON.stringify({ orderID: data.orderID }),43 });44 const details = await response.json();45 if (!response.ok) throw new Error('Capture failed');46 onSuccess?.(details);47 }}48 onError={(err) => {49 setError('Payment failed. Please try again.');50 console.error('PayPal error:', err);51 }}52 />53 {error && (54 <p className="text-red-600 text-sm mt-2">{error}</p>55 )}56 </div>57 </PayPalScriptProvider>58 );59}Expected result: The PayPal Smart Payment Button renders on your page showing PayPal, Venmo, and Debit/Credit Card options. Clicking it opens the PayPal sandbox popup. Using your sandbox buyer account credentials, you can complete a test payment.
Deploy and Register Webhooks for Payment Confirmation
Deploy and Register Webhooks for Payment Confirmation
During development in Bolt's WebContainer, you can test the complete payment flow using the PayPal popup — the createOrder and captureOrder steps work in the preview since they make outbound HTTP calls to PayPal's sandbox. However, PayPal also sends webhook events (order.approved, payment.capture.completed) to confirm payments asynchronously on your server. These webhooks require a publicly accessible URL that PayPal can call. Bolt's WebContainer has no public URL, so webhook registration must wait until after deployment. Deploy to Netlify or Bolt Cloud first. After deployment, go to developer.paypal.com → your app → Webhooks → Add Webhook. Enter your deployed URL (e.g., https://your-app.netlify.app/api/paypal/webhook) and select the events you need. Create the webhook handler API route and verify signatures using PayPal's webhook verification API. Set your production PayPal credentials (live client ID and secret) in your hosting platform's environment variables.
Create a webhook handler at /api/paypal/webhook that receives PayPal webhook events. It should verify the webhook signature using PayPal's verification API (POST to PAYPAL_API_URL/v1/notifications/verify-webhook-signature). For payment.capture.completed events, extract the order ID and amount and update the order status in the database to 'paid'. Return 200 for all valid webhooks.
Paste this in Bolt.new chat
Pro tip: Compare PayPal's manual setup (these 5 steps) to Stripe's integration: in Bolt, you can set up Stripe with a single chat prompt 'Add Stripe payments' and Bolt handles everything automatically. For new projects without existing PayPal requirements, Stripe is significantly easier.
Expected result: Your deployed app processes real PayPal payments in the PayPal sandbox. The webhook endpoint at /api/paypal/webhook receives payment confirmation events from PayPal. Order status updates correctly in your database when payments complete.
Common use cases
Single Product Checkout Page
Add a PayPal payment option to a product page for one-time purchases. Customers click the PayPal button, log into their PayPal account, and complete payment without entering card details. Your server captures the payment and updates the order status in your database.
Add a PayPal checkout button to my product page. The product costs $29.99 USD. Create an API route at /api/paypal/create-order that gets a PayPal OAuth token using PAYPAL_CLIENT_ID and PAYPAL_CLIENT_SECRET from environment variables, then creates a PayPal order for $29.99. Create another route at /api/paypal/capture-order that captures the approved order. Use the @paypal/paypal-js package to render the PayPal button on the product page. Use PayPal sandbox credentials for now.
Copy this prompt to try it in Bolt.new
Shopping Cart with Variable Amounts
Implement PayPal checkout for a shopping cart where the total varies based on items. The client sends the cart total to the create-order API route, which creates a PayPal order with the exact amount. This handles discounts, shipping calculations, and tax correctly.
Add PayPal checkout to my shopping cart. When the user clicks 'Pay with PayPal', call /api/paypal/create-order with the cart items array and total amount. The API route should create a PayPal order with the total amount and return the order ID. Use PayPalButtons from @paypal/react-paypal-js to show the checkout button. On approval, call /api/paypal/capture-order to complete the payment, then show a success page and clear the cart.
Copy this prompt to try it in Bolt.new
Donation Button
Add a flexible donation feature where donors can choose their own amount. PayPal's checkout flow handles the amount collection on the PayPal side, or you can let users input the amount on your page before initiating the PayPal flow.
Create a donation page with PayPal. Show preset donation amounts ($5, $10, $25, $50) and a custom amount input. When the user selects an amount and clicks 'Donate via PayPal', create a PayPal order for that amount via /api/paypal/create-order, then show the PayPal button panel. After successful payment, show a thank-you message with the donation amount and a transaction ID.
Copy this prompt to try it in Bolt.new
Troubleshooting
PayPal button does not render — blank space where button should appear
Cause: The NEXT_PUBLIC_PAYPAL_CLIENT_ID is missing or incorrect, or PayPalScriptProvider failed to load the external PayPal SDK script.
Solution: Check the browser console for script loading errors. Verify NEXT_PUBLIC_PAYPAL_CLIENT_ID is set in .env and has the NEXT_PUBLIC_ prefix (required for Next.js client-side access). The client ID should start with 'A' for sandbox or 'A' for live. Ensure you're using the correct environment (sandbox vs live) — sandbox client IDs only work with the sandbox API URL.
API route returns 401: 'Authentication failed' when calling PayPal token endpoint
Cause: The Base64 encoding of clientId:secret is incorrect, or the PAYPAL_CLIENT_SECRET environment variable is missing.
Solution: Verify your credentials are copied correctly without extra spaces. The Authorization header for PayPal OAuth uses Basic authentication with Base64-encoded 'clientId:secret'. Check that PAYPAL_CLIENT_SECRET is in your .env without NEXT_PUBLIC_ prefix. Confirm you're using sandbox credentials with the sandbox URL (api-m.sandbox.paypal.com).
1// Verify the auth header construction:2const auth = Buffer.from(3 `${process.env.NEXT_PUBLIC_PAYPAL_CLIENT_ID}:${process.env.PAYPAL_CLIENT_SECRET}`4).toString('base64');5console.log('Auth header:', `Basic ${auth}`.substring(0, 20) + '...'); // debug onlyPayPal webhooks not arriving after deployment
Cause: The webhook URL is not registered in the PayPal developer dashboard, or the deployed URL is incorrect.
Solution: Go to developer.paypal.com → Apps & Credentials → your app → Webhooks → Add Webhook. Enter your exact deployed URL including the path (e.g., https://your-app.netlify.app/api/paypal/webhook). Select the payment.capture.completed and checkout.order.approved event types. Test the webhook using PayPal's webhook simulator in the developer dashboard.
Payment works in sandbox but fails with 'INSTRUMENT_DECLINED' in production
Cause: The buyer's PayPal account has restrictions, the payment method was declined, or you're using sandbox credentials in production.
Solution: Verify you've switched to live credentials (live client ID and secret) in your production environment variables. INSTRUMENT_DECLINED is a buyer-side error — the buyer's bank or PayPal account declined the payment. This is not a code error. Check PayPal's error response body for the decline_code which indicates the specific reason.
Best practices
- Always compare PayPal's manual setup effort against Stripe's native one-prompt Bolt integration — choose PayPal only when your users specifically require it or when PayPal's buyer protection is a selling point
- Never store your PayPal Client Secret in client-side code or NEXT_PUBLIC_ environment variables — it must only be read by server-side API routes
- Cache PayPal access tokens (valid for 9 hours) to avoid requesting a new OAuth token on every payment — store the token and expiry in memory or Redis
- Always capture orders immediately after approval rather than using the AUTHORIZE intent for delayed capture, unless you have a specific delayed-charge use case
- Test with multiple PayPal sandbox accounts (different countries, currencies) to catch international payment issues before going live
- Implement idempotency in your order creation — if a network error causes the user to retry, don't create duplicate orders in your database
- Verify webhook signatures using PayPal's verification API rather than trusting webhook bodies without validation
- Switch your PAYPAL_API_URL from api-m.sandbox.paypal.com to api-m.paypal.com using an environment variable, not by changing code, when going from testing to production
Alternatives
Stripe is Bolt's native payment integration and requires only a single chat prompt to set up — far less effort than PayPal's manual implementation, making it the default choice for most Bolt projects.
Stripe Connect is better for marketplace platforms where you need to split payments between sellers and take a platform fee.
Braintree (owned by PayPal) offers a more developer-friendly API than standard PayPal while still supporting PayPal as a payment method alongside cards.
Coinbase Commerce enables cryptocurrency payments for customers who prefer to pay with Bitcoin, Ethereum, or other cryptocurrencies instead of traditional payment methods.
Frequently asked questions
Does Bolt.new have a native PayPal integration?
No. Bolt.new's native payment integration is exclusive to Stripe. PayPal must be implemented manually using PayPal's REST APIs through a Next.js API route and the @paypal/react-paypal-js client library. The manual setup takes approximately 30 minutes and involves more steps than Stripe's one-prompt setup.
Can I test PayPal payments in the Bolt.new preview?
Partially. You can test the order creation flow (creating a PayPal order and seeing the PayPal checkout popup) in the Bolt preview since these are outbound HTTP calls. However, PayPal webhook events (confirming payment completion) require a publicly accessible URL that Bolt's WebContainer cannot provide. Complete end-to-end payment testing requires deploying to Netlify or Bolt Cloud first.
How do I switch from PayPal sandbox to live payments?
Create a separate Live app in the PayPal developer dashboard and note the live client ID and secret. Update your environment variables in your hosting platform (Netlify/Vercel) to use live credentials and change PAYPAL_API_URL to https://api-m.paypal.com. Register your production webhook endpoint using the live app's webhook settings. Your code requires no changes — all credentials are read from environment variables.
Why is Stripe recommended over PayPal for Bolt.new projects?
Stripe is Bolt's only native payment integration and can be set up with a single chat prompt like 'Add Stripe checkout to my app' — Bolt auto-generates all the code, edge functions, and webhook handling. PayPal requires manually building the OAuth flow, order creation routes, checkout component, and webhook handler. For new projects without existing PayPal requirements, Stripe's developer experience is significantly better.
Can I offer both PayPal and Stripe on the same checkout page?
Yes. You can show both payment options by implementing Stripe's checkout for card payments (using Bolt's native integration) and adding the PayPal button component alongside it. Both APIs route through separate API routes and operate independently. Many e-commerce apps offer PayPal as an additional checkout method alongside card payments to maximize conversion by letting customers choose their preferred method.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation