Skip to main content
RapidDev - Software Development Agency
lovable-integrationsEdge Function Integration

How to Integrate Lovable with PayPal

Integrating PayPal with Lovable requires building Edge Functions to proxy the PayPal Checkout API, since PayPal has no native Lovable connector. You store your PayPal Client ID and Secret in Cloud Secrets, generate OAuth2 access tokens server-side, and use the PayPal JavaScript SDK on the frontend for the payment button. Expect 30–60 minutes of setup for a working checkout flow.

What you'll learn

  • How to store PayPal Client ID and Secret safely in Lovable Cloud Secrets
  • How to generate a PayPal OAuth2 access token inside a Deno Edge Function
  • How to create and capture PayPal orders via Edge Function proxy calls
  • How to render a PayPal checkout button using the PayPal JavaScript SDK
  • How to verify PayPal webhook notifications server-side in an Edge Function
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate17 min read45 minutesPaymentMarch 2026RapidDev Engineering Team
TL;DR

Integrating PayPal with Lovable requires building Edge Functions to proxy the PayPal Checkout API, since PayPal has no native Lovable connector. You store your PayPal Client ID and Secret in Cloud Secrets, generate OAuth2 access tokens server-side, and use the PayPal JavaScript SDK on the frontend for the payment button. Expect 30–60 minutes of setup for a working checkout flow.

Why integrate PayPal with Lovable?

PayPal reaches over 430 million active accounts worldwide, and in many markets — particularly Latin America, Southeast Asia, and Germany — a significant portion of online shoppers prefer PayPal over card-only checkout. If your Lovable app needs to accept payments from customers who either don't have a credit card or simply trust PayPal more than entering card details, adding PayPal alongside (or instead of) Stripe opens your revenue funnel to a much broader audience.

Unlike Stripe, which has a native Lovable connector with AI-assisted setup, PayPal requires a fully manual Edge Function integration. This means you write TypeScript code running on Deno to handle authentication, order management, and webhook verification. The process has more steps than Stripe, but the architecture is clean: your frontend loads the PayPal JS SDK to show the familiar PayPal button, clicks trigger Edge Function calls to create an order, and the SDK confirms payment before your Edge Function captures and records it.

PayPal also supports Venmo checkout (US), Pay Later (buy now, pay later), and recurring billing via the Subscriptions API — all accessible from the same credential set once your initial integration is working. This tutorial covers the core one-time payment flow, which is the foundation for all other PayPal features.

Integration method

Edge Function Integration

PayPal has no native Lovable connector. All authenticated PayPal API calls — OAuth2 token generation, order creation, and order capture — run through Supabase Edge Functions on Deno. The PayPal JavaScript SDK loads on the frontend to render the PayPal button, but it communicates with your Edge Functions rather than calling PayPal directly, keeping credentials secure in Cloud Secrets.

Prerequisites

  • A Lovable project with Cloud enabled (the default for all new projects)
  • A PayPal Business account — sign up free at developer.paypal.com
  • PayPal Developer app credentials (Client ID and Secret) from the PayPal Developer Dashboard
  • Familiarity with Lovable's chat interface and Cloud tab navigation
  • Your project deployed (or using a deployment preview URL) since PayPal Sandbox requires a real URL for webhook registration

Step-by-step guide

1

Create a PayPal Developer app and get your credentials

Before writing any code, you need PayPal API credentials. Go to developer.paypal.com and log in with your PayPal Business account. In the top navigation, click 'My Apps & Credentials'. Under the 'REST API apps' section, click 'Create App'. Give your app a name (e.g., 'My Lovable App') and select 'Merchant' as the account type. Click 'Create App' to confirm. PayPal will generate a Sandbox Client ID and Sandbox Secret for testing, and later a Live Client ID and Live Secret for production. On the app detail page you'll see both environments — start with Sandbox for all development. Copy the Sandbox Client ID (starts with AV...) and Sandbox Secret. You'll need both in the next step. Also on this page, scroll down to 'Webhooks' and note the section — you'll return here in Step 5 to register your webhook URL. At the bottom of the app settings, ensure 'Accept payments', 'Send payouts', and 'Transaction search' are all checked under Features, then save. This grants your credentials permission to use the Checkout and Payouts APIs.

Pro tip: PayPal Sandbox and Live are completely separate environments with separate credentials. Never mix them — using Live credentials in Sandbox mode or vice versa causes immediate authentication failures.

Expected result: You have a Sandbox Client ID and Sandbox Secret copied and ready. Your PayPal Developer app has the Checkout API feature enabled.

2

Store PayPal credentials in Lovable Cloud Secrets

With your credentials ready, open your Lovable project. Click the '+' button in the top-right area of the editor to open the panels menu, then select 'Cloud'. Inside the Cloud tab, scroll down to the 'Secrets' section and click it to expand. You'll see a form with 'Name' and 'Value' fields. Add your first secret: set Name to PAYPAL_CLIENT_ID and paste your Sandbox Client ID as the Value. Click 'Add Secret'. Then add a second secret: Name = PAYPAL_CLIENT_SECRET, Value = your Sandbox Secret. Click 'Add Secret' again. These secrets are now encrypted and stored server-side. They are accessible only from Edge Functions via Deno.env.get() — they will never appear in your frontend JavaScript bundle or browser network requests. Lovable's security system blocks approximately 1,200 hardcoded API keys per day from being committed to code; storing them here instead is the only safe approach. For production, you'll later come back and add PAYPAL_CLIENT_ID_LIVE and PAYPAL_CLIENT_SECRET_LIVE (or replace the existing secrets after thorough testing). Keep Sandbox credentials active throughout development.

Pro tip: Never paste your PayPal Client Secret into the Lovable chat window. On the free tier, chat history can be publicly visible and keys can be recovered from git commit history.

Expected result: PAYPAL_CLIENT_ID and PAYPAL_CLIENT_SECRET appear in your Cloud Secrets list. Neither value is visible in your project's source code.

3

Create the PayPal token + order Edge Functions

Now you'll create the server-side logic. In the Lovable chat, describe what you need and Lovable will generate the Edge Function code. The core function needs to do three things: (1) generate a PayPal OAuth2 access token using your Client ID and Secret, (2) create a PayPal order when the customer initiates checkout, and (3) capture the order after PayPal confirms customer approval. Paste the following prompt into Lovable chat, then review the generated Edge Function code carefully before it deploys: The generated Edge Function lives at supabase/functions/paypal-checkout/index.ts in your project. It uses Deno.env.get('PAYPAL_CLIENT_ID') and Deno.env.get('PAYPAL_CLIENT_SECRET') to authenticate. The token endpoint is https://api-m.sandbox.paypal.com/v1/oauth2/token for Sandbox (change to https://api-m.paypal.com for production). Order creation hits /v2/checkout/orders and capture hits /v2/checkout/orders/{id}/capture. The function should accept an action parameter in the POST body: 'create' to create an order and return an order ID, and 'capture' to capture an approved order given an order ID. Both actions require a fresh access token, which should be fetched inside the function on each request (tokens expire after 9 hours but it's simpler and safer to fetch fresh ones).

Lovable Prompt

Create a Supabase Edge Function at supabase/functions/paypal-checkout/index.ts that handles PayPal Checkout. It should read PAYPAL_CLIENT_ID and PAYPAL_CLIENT_SECRET from Deno.env. Support two actions via POST body: action='create' creates a PayPal order for a given amount and currency and returns the order_id; action='capture' captures an approved order given order_id and returns the capture details. Use the PayPal Sandbox API base URL https://api-m.sandbox.paypal.com. Fetch a fresh OAuth2 access token for each request. Add CORS headers so the frontend can call this function.

Paste this in Lovable chat

supabase/functions/paypal-checkout/index.ts
1// supabase/functions/paypal-checkout/index.ts
2import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
3
4const PAYPAL_BASE = Deno.env.get("PAYPAL_ENV") === "live"
5 ? "https://api-m.paypal.com"
6 : "https://api-m.sandbox.paypal.com";
7
8async function getAccessToken(): Promise<string> {
9 const clientId = Deno.env.get("PAYPAL_CLIENT_ID") ?? "";
10 const clientSecret = Deno.env.get("PAYPAL_CLIENT_SECRET") ?? "";
11 const credentials = btoa(`${clientId}:${clientSecret}`);
12
13 const res = await fetch(`${PAYPAL_BASE}/v1/oauth2/token`, {
14 method: "POST",
15 headers: {
16 Authorization: `Basic ${credentials}`,
17 "Content-Type": "application/x-www-form-urlencoded",
18 },
19 body: "grant_type=client_credentials",
20 });
21 const data = await res.json();
22 if (!res.ok) throw new Error(data.error_description ?? "Token fetch failed");
23 return data.access_token;
24}
25
26serve(async (req) => {
27 if (req.method === "OPTIONS") {
28 return new Response(null, {
29 headers: {
30 "Access-Control-Allow-Origin": "*",
31 "Access-Control-Allow-Methods": "POST, OPTIONS",
32 "Access-Control-Allow-Headers": "Content-Type, Authorization",
33 },
34 });
35 }
36
37 try {
38 const { action, amount, currency, orderId } = await req.json();
39 const accessToken = await getAccessToken();
40
41 if (action === "create") {
42 const res = await fetch(`${PAYPAL_BASE}/v2/checkout/orders`, {
43 method: "POST",
44 headers: {
45 Authorization: `Bearer ${accessToken}`,
46 "Content-Type": "application/json",
47 },
48 body: JSON.stringify({
49 intent: "CAPTURE",
50 purchase_units: [{
51 amount: { currency_code: currency ?? "USD", value: amount },
52 }],
53 }),
54 });
55 const order = await res.json();
56 return new Response(JSON.stringify({ orderId: order.id }), {
57 headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" },
58 });
59 }
60
61 if (action === "capture") {
62 const res = await fetch(`${PAYPAL_BASE}/v2/checkout/orders/${orderId}/capture`, {
63 method: "POST",
64 headers: {
65 Authorization: `Bearer ${accessToken}`,
66 "Content-Type": "application/json",
67 },
68 });
69 const capture = await res.json();
70 return new Response(JSON.stringify(capture), {
71 headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" },
72 });
73 }
74
75 return new Response(JSON.stringify({ error: "Unknown action" }), { status: 400 });
76 } catch (err) {
77 return new Response(JSON.stringify({ error: err.message }), {
78 status: 500,
79 headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" },
80 });
81 }
82});

Pro tip: The PAYPAL_ENV secret is optional — if you add it with value 'live' in Cloud Secrets, the function automatically switches to the production PayPal API. During development keep it absent or set to 'sandbox'.

Expected result: The Edge Function is deployed and visible in Cloud → Edge Functions. Lovable confirms deployment with no errors in the Cloud Logs.

4

Add the PayPal JavaScript SDK and button to your frontend

With the Edge Functions deployed, you now need a PayPal button on your page. PayPal provides an official JavaScript SDK that renders the button and handles the customer-facing payment flow. You load this SDK with a script tag that includes your publishable Client ID — this key is safe to expose in the frontend because it only allows initiating payment flows, not charging cards directly. Ask Lovable to add the PayPal button to your checkout page. The button component needs to: (1) load the PayPal JS SDK with your Client ID, (2) on button click, call your paypal-checkout Edge Function with action='create' to get an order ID, (3) return that order ID to the SDK so it can redirect the customer to PayPal approval, (4) after customer approval, call the Edge Function with action='capture' and the order ID, and (5) on successful capture, update the UI and save the transaction to your database. The PAYPAL_CLIENT_ID you use in the frontend script tag is your Sandbox Client ID. This is the same credential you stored in secrets, but its frontend exposure is intentional and safe — PayPal designed the SDK this way. Your Client Secret never appears in any frontend code. Make sure to use your Supabase project URL when calling the Edge Function (visible in Cloud tab or Project Settings).

Lovable Prompt

Add a PayPal checkout button to the /checkout page. Load the PayPal JavaScript SDK using script tag with my PAYPAL_CLIENT_ID (use the value from an environment variable VITE_PAYPAL_CLIENT_ID). The button should: 1) call the paypal-checkout Edge Function with action='create' and amount='49.00' to get an orderId, 2) return that orderId to the PayPal SDK, 3) after approval call the Edge Function with action='capture' and the orderId, 4) on success show a confirmation message and save the transaction to a purchases table in Supabase with columns: user_id, paypal_order_id, amount, currency, status, created_at.

Paste this in Lovable chat

src/components/PayPalButton.tsx
1// src/components/PayPalButton.tsx
2import { useEffect, useRef } from "react";
3import { supabase } from "@/integrations/supabase/client";
4
5declare global {
6 interface Window { paypal: any; }
7}
8
9interface PayPalButtonProps {
10 amount: string;
11 currency?: string;
12 onSuccess?: (orderId: string) => void;
13}
14
15export function PayPalButton({ amount, currency = "USD", onSuccess }: PayPalButtonProps) {
16 const buttonRef = useRef<HTMLDivElement>(null);
17 const clientId = import.meta.env.VITE_PAYPAL_CLIENT_ID;
18
19 useEffect(() => {
20 const script = document.createElement("script");
21 script.src = `https://www.paypal.com/sdk/js?client-id=${clientId}&currency=${currency}`;
22 script.async = true;
23 script.onload = () => {
24 if (!buttonRef.current) return;
25 window.paypal.Buttons({
26 createOrder: async () => {
27 const { data } = await supabase.functions.invoke("paypal-checkout", {
28 body: { action: "create", amount, currency },
29 });
30 return data.orderId;
31 },
32 onApprove: async (_data: any, actions: any) => {
33 const orderId = _data.orderID;
34 const { data } = await supabase.functions.invoke("paypal-checkout", {
35 body: { action: "capture", orderId },
36 });
37 if (data?.status === "COMPLETED") {
38 onSuccess?.(orderId);
39 }
40 },
41 onError: (err: any) => console.error("PayPal error", err),
42 }).render(buttonRef.current);
43 };
44 document.body.appendChild(script);
45 return () => { document.body.removeChild(script); };
46 }, [amount, currency, clientId]);
47
48 return <div ref={buttonRef} className="w-full max-w-sm mx-auto" />;
49}

Pro tip: Add VITE_PAYPAL_CLIENT_ID to your project's environment variables in Lovable (not Cloud Secrets — this is a public build-time variable). In the Cloud tab under Secrets, Lovable distinguishes between VITE_ prefixed variables (build time, exposed to frontend) and non-prefixed variables (server-side only).

Expected result: A PayPal button renders on the checkout page. Clicking it opens the PayPal payment popup in Sandbox mode. After completing a test payment in the sandbox, a success message appears.

5

Set up PayPal Webhook notifications for payment confirmation

Relying solely on the frontend callback to confirm payment is insecure — a malicious user could call your capture endpoint without actually paying. The production-safe pattern is to also listen for PayPal's server-side webhook notification, which PayPal sends directly to your backend after every successful payment. Create a webhook receiver Edge Function in Lovable. This function will receive POST requests from PayPal at a URL like https://[your-project].supabase.co/functions/v1/paypal-webhook. Inside the function, you verify the webhook signature using PayPal's Verify Webhook Signature API — this confirms the event actually came from PayPal and wasn't forged. After creating the Edge Function, go back to your PayPal Developer Dashboard, open your app, scroll to 'Webhooks', and click 'Add Webhook'. Enter your Edge Function URL and select the event types to listen for: CHECKOUT.ORDER.APPROVED and PAYMENT.CAPTURE.COMPLETED are the two most important for a checkout flow. PayPal will generate a Webhook ID — save this as PAYPAL_WEBHOOK_ID in Cloud Secrets. For complex webhook architectures or if you need to handle subscriptions, disputes, and refund events simultaneously, RapidDev's team can help design a robust event-routing system using a single webhook endpoint that dispatches to different handlers based on event type.

Lovable Prompt

Create a Supabase Edge Function at supabase/functions/paypal-webhook/index.ts that receives PayPal webhook events. It should: 1) read the PayPal-Transmission-Id, PayPal-Transmission-Time, PayPal-Cert-Url, PayPal-Transmission-Sig, and PayPal-Auth-Algo headers, 2) verify the webhook signature by calling PayPal's verify-webhook-signature API using PAYPAL_CLIENT_ID, PAYPAL_CLIENT_SECRET, and PAYPAL_WEBHOOK_ID from Deno.env, 3) on PAYMENT.CAPTURE.COMPLETED events, update the purchases table in Supabase to set status='completed' where paypal_order_id matches the resource.supplementary_data.related_ids.order_id field.

Paste this in Lovable chat

supabase/functions/paypal-webhook/index.ts
1// supabase/functions/paypal-webhook/index.ts
2import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
3import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
4
5const PAYPAL_BASE = "https://api-m.sandbox.paypal.com";
6
7async function getAccessToken(): Promise<string> {
8 const creds = btoa(`${Deno.env.get("PAYPAL_CLIENT_ID")}:${Deno.env.get("PAYPAL_CLIENT_SECRET")}`);
9 const res = await fetch(`${PAYPAL_BASE}/v1/oauth2/token`, {
10 method: "POST",
11 headers: { Authorization: `Basic ${creds}`, "Content-Type": "application/x-www-form-urlencoded" },
12 body: "grant_type=client_credentials",
13 });
14 return (await res.json()).access_token;
15}
16
17serve(async (req) => {
18 const body = await req.text();
19 const headers = Object.fromEntries(req.headers.entries());
20
21 const token = await getAccessToken();
22 const verifyRes = await fetch(`${PAYPAL_BASE}/v1/notifications/verify-webhook-signature`, {
23 method: "POST",
24 headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
25 body: JSON.stringify({
26 auth_algo: headers["paypal-auth-algo"],
27 cert_url: headers["paypal-cert-url"],
28 transmission_id: headers["paypal-transmission-id"],
29 transmission_sig: headers["paypal-transmission-sig"],
30 transmission_time: headers["paypal-transmission-time"],
31 webhook_id: Deno.env.get("PAYPAL_WEBHOOK_ID"),
32 webhook_event: JSON.parse(body),
33 }),
34 });
35 const { verification_status } = await verifyRes.json();
36 if (verification_status !== "SUCCESS") {
37 return new Response("Verification failed", { status: 400 });
38 }
39
40 const event = JSON.parse(body);
41 if (event.event_type === "PAYMENT.CAPTURE.COMPLETED") {
42 const supabase = createClient(
43 Deno.env.get("SUPABASE_URL") ?? "",
44 Deno.env.get("SUPABASE_SERVICE_ROLE_KEY") ?? ""
45 );
46 const orderId = event.resource?.supplementary_data?.related_ids?.order_id;
47 if (orderId) {
48 await supabase.from("purchases").update({ status: "completed" }).eq("paypal_order_id", orderId);
49 }
50 }
51
52 return new Response("OK", { status: 200 });
53});

Pro tip: Always return HTTP 200 to PayPal webhooks quickly, even if your database update fails. PayPal will retry unacknowledged webhooks up to 25 times over 3 days — handle idempotency by checking if the order is already marked completed before updating.

Expected result: The webhook Edge Function is deployed. In PayPal Developer Dashboard, the webhook shows as registered with a green checkmark. You can click 'Simulate' to send a test event and verify it reaches your function via Cloud Logs.

6

Switch to PayPal Live credentials for production

Once you've tested end-to-end in Sandbox mode and confirmed orders are created, captured, and stored correctly, you're ready to go live. The switch requires updating three secrets and one frontend environment variable. In Cloud → Secrets, update PAYPAL_CLIENT_ID to your Live Client ID and PAYPAL_CLIENT_SECRET to your Live Client Secret (both found in the PayPal Developer Dashboard under your app's 'Live' tab). Add a new secret PAYPAL_ENV with value 'live' — the Edge Function code from Step 3 reads this to switch the API base URL automatically. For PAYPAL_WEBHOOK_ID, you'll need to register a new webhook in the Live environment (PayPal Sandbox and Live have separate webhook registrations). Go to your app in the PayPal Developer Dashboard, click the 'Live' tab, scroll to Webhooks, and register your same Edge Function URL. Update PAYPAL_WEBHOOK_ID in Cloud Secrets to the new Live Webhook ID. Finally, update VITE_PAYPAL_CLIENT_ID in your project's build environment variables to the Live Client ID. Redeploy your app from the Publish button. Test with a real payment of $0.01 to confirm the live flow works, then refund it through the PayPal dashboard. Never process a test payment with a large amount on Live credentials during verification.

Pro tip: PayPal Live accounts require your business account to be verified with bank account or debit card before you can receive real payments. Check your PayPal Business account status at paypal.com/businesswallet before going live to avoid payment failures at checkout.

Expected result: Your app processes real PayPal payments. The PayPal button shows the live PayPal branding (not Sandbox). Completed payments appear in your PayPal Business account dashboard within seconds.

Common use cases

Accept one-time product payments with PayPal button

A founder selling a digital product (ebook, template pack, or course) wants to offer PayPal checkout alongside Stripe so international customers can pay without a credit card. The PayPal button appears on the product page and the Edge Function creates and captures the order, then updates the database to unlock the purchased content.

Lovable Prompt

Add a PayPal checkout button to the /buy page for a $49 product. When clicked it should call an Edge Function to create a PayPal order, then capture it after the customer approves. Store the transaction in the purchases table with status, amount, and paypal_order_id.

Copy this prompt to try it in Lovable

International marketplace payouts to sellers

A two-sided marketplace needs to pay out seller earnings in countries where Stripe Payouts aren't available. PayPal Payouts API lets you batch-send money to seller PayPal accounts using just an email address, making it practical for international micro-payouts without requiring sellers to have bank accounts.

Lovable Prompt

Create an admin Edge Function that sends PayPal payouts to sellers. It should read pending_payouts from the database where paid=false, batch them using the PayPal Payouts API, and mark each row paid with the payout_batch_id returned by PayPal.

Copy this prompt to try it in Lovable

Subscription billing via PayPal Billing Agreements

A SaaS app wants to offer monthly subscriptions to customers who prefer PayPal over card billing. Using the PayPal Subscriptions API, an Edge Function creates a subscription plan and generates a hosted approval URL the customer clicks to authorize recurring charges — PayPal then handles all future billing automatically.

Lovable Prompt

Add a PayPal subscription option to the /pricing page for a $19/month plan. The Edge Function should create a PayPal subscription using the Subscriptions API and redirect the user to PayPal for approval. On return, save the subscription_id to the user's profile in Supabase.

Copy this prompt to try it in Lovable

Troubleshooting

Edge Function returns 401 Unauthorized when calling PayPal API

Cause: The PAYPAL_CLIENT_ID or PAYPAL_CLIENT_SECRET secret is missing, misspelled, or contains extra whitespace. The Base64 encoding of invalid credentials produces an authorization header that PayPal rejects.

Solution: Open Cloud → Secrets in Lovable and verify both PAYPAL_CLIENT_ID and PAYPAL_CLIENT_SECRET are present with exact values from the PayPal Developer Dashboard. Check for leading/trailing spaces in the values. Also confirm you're using Sandbox credentials with the Sandbox API URL (api-m.sandbox.paypal.com) and not mixing environments.

PayPal button renders but clicking it shows 'This transaction cannot be processed'

Cause: The order amount is formatted incorrectly. PayPal requires the amount value to be a string with exactly 2 decimal places (e.g., '49.00' not '49' or 49). Sending an integer or a number without decimals causes PayPal's order validation to fail.

Solution: Ensure the amount passed to the Edge Function and used in the order creation body is always a string formatted with two decimal places. In your frontend component, use amount.toFixed(2) or format the value as a string before passing it.

typescript
1// Format amount correctly before passing to Edge Function
2const formattedAmount = parseFloat(rawAmount).toFixed(2); // '49.00'
3
4// In the Edge Function order body:
5body: JSON.stringify({
6 intent: "CAPTURE",
7 purchase_units: [{ amount: { currency_code: "USD", value: formattedAmount } }],
8})

Webhook verification fails with 'INVALID_RESOURCE_ID' error from PayPal

Cause: The PAYPAL_WEBHOOK_ID secret does not match the webhook registered in the PayPal Developer Dashboard. PayPal uses the Webhook ID to look up the expected signing certificate — a mismatch causes all verification attempts to fail.

Solution: Go to PayPal Developer Dashboard → Your App → Webhooks. Copy the exact Webhook ID shown (not the URL, but the ID like 5WB63946DE559564C). Update PAYPAL_WEBHOOK_ID in Cloud Secrets with this value. Ensure you're copying from the correct environment (Sandbox vs Live).

CORS error in browser console when PayPal button calls the Edge Function

Cause: The Edge Function is missing the Access-Control-Allow-Origin response header, or the OPTIONS preflight request is not handled. Browsers send an OPTIONS request before POST requests to cross-origin URLs, and if the Edge Function doesn't respond correctly, the browser blocks the call.

Solution: Ensure your Edge Function handles the OPTIONS method and returns the required CORS headers. The fix below shows the correct preflight handler. Add it before your main request handling logic.

typescript
1if (req.method === "OPTIONS") {
2 return new Response(null, {
3 headers: {
4 "Access-Control-Allow-Origin": "*",
5 "Access-Control-Allow-Methods": "POST, OPTIONS",
6 "Access-Control-Allow-Headers": "Content-Type, Authorization",
7 },
8 });
9}

Best practices

  • Always fetch a fresh OAuth2 access token inside each Edge Function invocation rather than caching it — tokens expire and a stale token causes silent failures in production.
  • Store PAYPAL_CLIENT_ID and PAYPAL_CLIENT_SECRET in Cloud Secrets, never in your frontend code or Lovable chat messages. Even the publishable Client ID used in the JS SDK should come from a VITE_ prefixed environment variable, not be hardcoded.
  • Verify webhook signatures server-side on every incoming PayPal webhook before acting on the event. Skipping verification allows any attacker to send fake payment confirmations to your endpoint.
  • Use idempotency when updating your database from webhooks — check if the order is already marked completed before updating to prevent double-processing from PayPal's retry logic.
  • Test every payment scenario in Sandbox before going live: successful payment, buyer cancellation, and declined payment. Use PayPal's test card numbers and sandbox buyer accounts from developer.paypal.com/tools/sandbox/accounts.
  • Keep Sandbox and Live credentials separate with clearly named secrets (PAYPAL_CLIENT_ID for Sandbox, PAYPAL_CLIENT_ID_LIVE for production, or use PAYPAL_ENV to switch). Mixing environments causes hard-to-debug failures.
  • Enable PayPal's 'Pay Later' option in the JS SDK to offer buy now, pay later to US customers at no additional cost — it can increase conversion rates significantly with no code changes beyond the script tag parameters.

Alternatives

Frequently asked questions

Does Lovable have a native PayPal integration?

No. As of March 2026, Lovable's 17 shared connectors include Stripe but not PayPal. PayPal requires a fully manual Edge Function integration where you proxy PayPal API calls server-side. This tutorial covers the complete setup. The upside is that once built, the integration is robust and uses PayPal's full API surface.

Can I use PayPal and Stripe at the same time in Lovable?

Yes. Because Stripe is a native connector and PayPal runs through custom Edge Functions, they operate independently and don't conflict. Many apps offer both to maximize customer choice — you can show a Stripe card form and a PayPal button side by side on the same checkout page.

Why does PayPal integration require Edge Functions while Stripe doesn't?

Stripe's native connector handles the Edge Function scaffolding automatically when you connect via Settings → Connectors. PayPal has no such connector, so you need to manually create the server-side proxy functions. Both approaches are equally secure — the difference is how much Lovable's AI assists with the setup.

Does the PayPal Sandbox require a real URL?

PayPal Sandbox webhooks require a publicly accessible HTTPS URL. Lovable's development preview uses a real URL, so webhook testing works during development. The PayPal JS SDK button itself works in any browser context, including the Lovable editor preview.

How do I handle PayPal refunds in Lovable?

Create a separate Edge Function that calls the PayPal Refunds API endpoint (/v2/payments/captures/{captureId}/refund). You'll need the capture ID returned during payment capture, which you should store in your purchases table. Full and partial refunds are both supported through the same endpoint by specifying an optional amount in the request body.

Can I use PayPal for recurring subscriptions in Lovable?

Yes, through PayPal's Subscriptions API. You first create a billing plan defining the price and interval, then create a subscription linked to that plan and redirect the customer to PayPal for approval. After approval, PayPal handles all future charges automatically and sends webhook notifications for each billing cycle. The Edge Function architecture from this tutorial is the same starting point.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.