# How to Add a Payment Gateway to Your App (Copy-Paste Prompts Included)

- Tool: App Features
- Last updated: July 2026

## TL;DR

A payment gateway needs five pieces: Stripe Elements or Checkout for the UI, a server-side Payment Intent handler, a webhook listener for fulfillment, an orders table in Supabase, and a confirmation email. With Lovable or V0 you can ship a working checkout in 3–6 hours. Running costs start near $0 and scale with transaction volume — Stripe charges 2.9% + $0.30 per charge, nothing until you earn.

## What a Payment Gateway Feature Actually Is

A payment gateway is the infrastructure that moves money from your customer's card or wallet to your bank account. In 2026 that means Stripe: it handles card tokenization, Apple Pay and Google Pay, 3D Secure authentication for EU cards, and fraud detection — without your server ever touching raw card numbers. The real product decisions are what happens around the payment: how you create the Payment Intent server-side, how your webhook listener fulfills orders reliably, and how you handle the edge cases that generate support tickets (declined cards, incomplete payments, duplicate webhook delivery). Get those right and the checkout just works.

## Anatomy of the Feature

Five components, two of which AI tools routinely get wrong. The webhook listener and idempotency logic are where first builds break under real traffic.

- **Checkout UI** (ui): Stripe Elements renders the card field, Apple Pay and Google Pay buttons, and the 3DS confirmation modal directly in your page. Stripe Checkout is an alternative hosted page Stripe controls — lower customization, faster to ship.
- **Payment Intent handler** (backend): A Supabase Edge Function or Next.js API route calls stripe.paymentIntents.create() server-side with the amount in cents, currency, metadata (user_id, order_id), and optionally a Stripe Customer ID for saved payment methods. The client secret returned is passed to Stripe Elements to complete the payment on the client.
- **Webhook listener** (backend): An Edge Function or API route receives Stripe events (payment_intent.succeeded, payment_intent.payment_failed, charge.refunded) and updates your orders table accordingly. Uses stripe.webhooks.constructEvent() with the raw request body and your webhook signing secret.
- **Orders table** (data): Supabase PostgreSQL stores order_id, user_id, stripe_payment_intent_id, stripe_customer_id, amount_cents, currency, status (pending / paid / failed / refunded), metadata jsonb, and created_at. Row-level security restricts each row to its owner.
- **Confirmation and receipt** (ui): A post-payment success screen shows order details, then a Supabase Edge Function triggers a transactional receipt email via Resend or SendGrid. Email is triggered by the payment_intent.succeeded webhook, not the client redirect — so it fires even if the user closes the tab before the redirect completes.

## Data model

Two tables cover the full payment flow. Run this in the Supabase SQL editor after connecting your project:

```sql
create table public.orders (
  id uuid primary key default gen_random_uuid(),
  user_id uuid references auth.users(id) on delete set null,
  stripe_payment_intent_id text unique not null,
  stripe_customer_id text,
  amount_cents integer not null check (amount_cents >= 50),
  currency text not null default 'usd',
  status text not null default 'pending'
    check (status in ('pending', 'paid', 'failed', 'refunded')),
  metadata jsonb default '{}',
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now()
);

alter table public.orders enable row level security;

create policy "Users can view own orders"
  on public.orders for select
  using (auth.uid() = user_id);

create table public.stripe_customers (
  id uuid primary key default gen_random_uuid(),
  user_id uuid references auth.users(id) on delete cascade not null unique,
  stripe_customer_id text not null unique,
  created_at timestamptz not null default now()
);

alter table public.stripe_customers enable row level security;

create policy "Users can view own Stripe customer record"
  on public.stripe_customers for select
  using (auth.uid() = user_id);

create index orders_user_id_created_idx
  on public.orders (user_id, created_at desc);

create index orders_payment_intent_idx
  on public.orders (stripe_payment_intent_id);
```

The UNIQUE constraint on stripe_payment_intent_id is what makes your webhook handler idempotent — a second delivery of the same event hits the constraint and does nothing instead of double-fulfilling.

## Build paths

### Lovable — fit 5/10, 3–5 hours

Best all-round path — Lovable's native Stripe connector generates the Edge Functions, Supabase schema, and auth wiring in one project. Deploy once and the webhooks work.

1. Create a new Lovable project, open Cloud tab, enable Supabase — this provisions the database and auth
2. Go to Cloud tab → Connectors → Stripe and connect your Stripe account (test mode first)
3. Paste the prompt below in Agent Mode and let it generate the checkout page, Edge Functions, and orders table
4. Click Publish — the Stripe connector only works on the published URL, not the in-editor preview iframe
5. In the Stripe Dashboard, add your published URL as a webhook endpoint and copy the signing secret into Lovable Cloud → Secrets as STRIPE_WEBHOOK_SECRET
6. Test with card 4242 4242 4242 4242, expiry 12/34, any CVC; confirm the order row appears in Supabase Table Editor

Starter prompt:

```
Build a complete payment checkout feature using Stripe. Use the Stripe native connector. Create a checkout page with Stripe Elements for the card field, Apple Pay, and Google Pay. On form submit, call an Edge Function that creates a Stripe Payment Intent server-side with amount in cents, currency 'usd', and metadata including the current user_id. Return the client_secret to the browser and confirm payment with stripe.confirmCardPayment(). After payment, show a confirmation screen with the order total and a 'View order history' link. Create a Stripe webhook Edge Function that handles payment_intent.succeeded (set order status to 'paid', trigger a confirmation email via Resend), payment_intent.payment_failed (set status to 'failed', show a toast with the decline reason and a retry button). Verify the webhook signature using constructEventAsync() with the raw Uint8Array body — never JSON.parse before verification. Check if the order already has status 'paid' before updating to prevent duplicate fulfillment. Use test card 4242 4242 4242 4242 for development. Handle 3DS cards (authenticate) by listening for the requires_action state. Scaffold two Supabase Secrets: STRIPE_SECRET_KEY for the server and STRIPE_WEBHOOK_SECRET for webhook verification. Include an order history page showing each order's amount, status, date, and a link to the Stripe-hosted receipt URL.
```

Limitations:

- Stripe Checkout redirect and Stripe Elements do not function inside the Lovable in-editor preview iframe — always test on the published URL
- Webhook testing requires your published domain — Stripe cannot deliver events to localhost
- Switching from test to live mode requires a new webhook endpoint with a new signing secret; prompt to scaffold both as separate Supabase Secrets
- Stripe Customer saved payment methods require additional Edge Function logic beyond the default connector setup

### V0 — fit 4/10, 4–6 hours

Excellent for web apps that are already Next.js — V0 generates clean API route code for Payment Intents and Checkout sessions, and Vercel deployments support webhook delivery natively.

1. Prompt V0 with the spec below to generate the checkout component and API routes
2. In the Vercel dashboard, add STRIPE_SECRET_KEY and STRIPE_WEBHOOK_SECRET to Environment Variables
3. Add NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY for the database connection
4. Run the SQL schema from this page in the Supabase SQL editor
5. Deploy to Vercel, then register your Vercel domain as a Stripe webhook endpoint in the Stripe Dashboard
6. Test end-to-end with the Stripe test card before switching keys to live mode

Starter prompt:

```
Build a complete Stripe payment checkout for a Next.js 14 App Router project. API route app/api/payment-intent/route.ts: POST handler that validates the amount (minimum 50 cents), calls stripe.paymentIntents.create() with amount, currency, and metadata (user_id from the session), returns {clientSecret}. API route app/api/webhooks/stripe/route.ts: reads the raw request body as a Buffer, verifies with stripe.webhooks.constructEvent(rawBody, signature, process.env.STRIPE_WEBHOOK_SECRET), handles payment_intent.succeeded (upsert order row with status 'paid' in Supabase, check stripe_payment_intent_id for idempotency before updating, trigger Resend receipt email), handles payment_intent.payment_failed (set status 'failed'). Client component CheckoutForm: uses @stripe/react-stripe-js with Elements provider; renders CardElement plus Apple Pay and Google Pay via PaymentRequestButton; shows loading spinner during confirmation; shows decline reason with retry button on failure; shows confirmation screen with order total on success. Handle 3DS authentication by catching the requires_action nextAction and calling stripe.handleNextAction(). Include an OrderHistory server component that fetches the user's orders from Supabase newest-first.
```

Limitations:

- No auto-provisioning — Stripe keys and Supabase credentials must be added manually to Vercel Environment Variables
- V0 sometimes stores checkout state in localStorage, which causes SSR hydration errors; prompt explicitly to use server-side session state
- The V0 sandbox preview cannot process real Stripe API calls — deploy to Vercel to test the full flow
- Occasional shadcn registry mismatches on Stripe Elements wrappers require a follow-up fix prompt

### Flutterflow — fit 3/10, 1–2 days

Mobile path for apps already in FlutterFlow — calls your Edge Function to create a Payment Intent, then presents the Stripe native payment sheet for Apple Pay and Google Pay support on device.

1. Create a Supabase Edge Function externally (or in Lovable) that accepts POST with amount and currency and returns a clientSecret
2. In FlutterFlow, add an API call action pointing to your Edge Function URL with your Supabase anon key header
3. Add the flutter_stripe package via FlutterFlow's custom code dependency manager
4. In a custom code block, call Stripe.instance.initPaymentSheet() with the returned clientSecret, then Stripe.instance.presentPaymentSheet() to show the native payment UI
5. Add an Action to handle success (update UI state, navigate to confirmation page) and error (show SnackBar with error message)
6. In FlutterFlow Settings → Permissions, add any required iOS entitlements for Apple Pay

Limitations:

- No official Stripe widget in FlutterFlow — native payment sheet requires a custom code block with flutter_stripe package
- Apple Pay and Google Pay native sheet requires additional entitlements and provisioning profile configuration outside FlutterFlow
- Webhook handling must live on an external server — FlutterFlow cannot host webhook endpoints
- FlutterFlow Pro or Teams plan required for custom code export needed to implement the payment sheet

### Custom — fit 5/10, 1–2 weeks

When your payment requirements go beyond standard checkout — marketplace split payments via Stripe Connect, per-seat billing, SCA compliance audits, or complex partial refund workflows with approval chains.

1. Full Stripe Connect implementation for multi-vendor marketplaces with application fees and automatic/manual payouts
2. SCA-compliant 3DS handling with custom authentication UI instead of Stripe's hosted redirect
3. Multi-currency support with currency conversion and localized pricing display
4. Partial refund workflows with role-based approval chain before Stripe refund API is called

Limitations:

- Minimum 1–2 weeks for a production-grade implementation including testing and PCI-aware architecture review
- Stripe Connect compliance and onboarding adds significant complexity beyond standard payment acceptance

## Gotchas

- **Webhook fires before the browser redirect completes — double fulfillment** — Stripe fires payment_intent.succeeded almost instantly after the card is charged — often before the browser has redirected to your success page. If your AI-generated handler marks an order fulfilled, and your success page also updates the order, you get a race condition or duplicate email. Worse: Stripe retries webhooks on any 5xx response, so a crashing handler can trigger multiple fulfillments. Fix: Use Stripe's idempotency key on Payment Intent creation (set idempotency_key: order_id). In the webhook handler, check the order's current status before updating — only transition from 'pending' to 'paid', never overwrite 'paid'. Add a UNIQUE index on stripe_payment_intent_id so a second INSERT fails gracefully.
- **Preview iframe blocks Stripe Checkout redirect — payment appears to hang** — In the Lovable in-editor preview and the V0 sandbox, Stripe Checkout's redirect opens inside a sandboxed iframe. The iframe security policy blocks the redirect, so the checkout page appears to freeze. This is often mistaken for a code bug and triggers hours of debugging. Fix: Always test payment flows on your published Lovable URL or Vercel deployment. Use the Stripe test card 4242 4242 4242 4242 on the live HTTPS URL — camera and payment APIs both require a real browser context outside iframes.
- **Webhook signature verification fails — 400 errors on every event** — Stripe signs webhook payloads with an HMAC using the raw request body bytes. If your handler calls JSON.parse() or response.json() before passing the body to stripe.webhooks.constructEvent(), the byte representation changes and the signature check always fails. This is the most common payment integration bug AI tools produce. Fix: In your Edge Function, read the request body as raw bytes: const body = await request.arrayBuffer(); const rawBody = new Uint8Array(body). Pass rawBody directly to constructEventAsync(). Never await request.json() before this step. Add a log line that prints the first 20 bytes to confirm you have the raw body before verification.
- **Live vs test mode mismatch — test orders appear in production** — AI tools build with Stripe test keys (sk_test_...). When you switch to live mode, Stripe issues separate webhook signing secrets for each endpoint — the test webhook secret does not verify live events. Apps that use a single STRIPE_SECRET environment variable silently mix test and live charges. Fix: Scaffold two Supabase Secret entries from the start: STRIPE_SECRET_KEY (initially test, swap to live at launch) and STRIPE_WEBHOOK_SECRET (one per webhook endpoint registration). Add a STRIPE_ENV flag ('test'|'live') your app reads to show an admin banner when running in test mode.
- **'Cannot make live charges' error after switching keys** — Stripe requires business details — legal name, address, bank account, and identity verification — before activating a live account. An app built with test keys that switches to live keys without completing Stripe Dashboard onboarding throws this error on every charge attempt. Users who encounter this see a generic payment failure. Fix: Before launching, complete Stripe Dashboard onboarding fully. In your admin panel, call stripe.accounts.retrieve() and check capabilities.card_payments.status === 'active'. Show a prominent 'Complete Stripe setup' banner to the account owner until this check passes.

## Best practices

- Always create Payment Intents server-side — never expose your Stripe secret key to the browser or allow amounts to be set client-side
- Use Stripe's idempotency keys on Payment Intent creation so retried requests return the same intent instead of charging twice
- Make your webhook handler idempotent: check the order's current status before updating, and use INSERT ... ON CONFLICT DO NOTHING for receipt records
- Test every decline scenario before launch: 4000000000000002 (generic decline), 4000000000009995 (insufficient funds), 4000000000009987 (lost card) — each produces a different decline_code your UI should handle specifically
- Store the Stripe receipt URL (charge.receipt_url) in your orders table so users can access it from their account without you building a PDF generator
- Trigger receipt emails from the webhook handler, not the success URL redirect — the client can close the tab before the redirect fires
- Display proration previews before plan upgrades using stripe.invoices.retrieveUpcoming() — surprise charges at upgrade are the top cause of subscription cancellations
- Scaffold two separate Stripe API key secrets (test and live) from the beginning; retrofitting this after launch is painful and error-prone

## Frequently asked questions

### Does Stripe work in all countries?

Stripe supports businesses in 46 countries and accepts cards from customers worldwide. The main limitation is where you, the business owner, can register — not where your customers are located. Check stripe.com/global for the current country list. If your country is not supported, Stripe Atlas can help you incorporate in the US.

### How do I switch from test to live mode safely?

Create a new webhook endpoint in the Stripe Dashboard pointing to your production URL and copy the new signing secret to your STRIPE_WEBHOOK_SECRET environment variable. Swap STRIPE_SECRET_KEY from sk_test_ to sk_live_. Test orders from test mode do not appear in live mode — you start with a clean slate. Complete Stripe Dashboard business verification before switching or all live charges will be declined.

### What happens if a payment fails midway — is the user charged?

If the charge fails — declined card, failed 3DS, or network error — no money moves. A Payment Intent in the 'requires_payment_method' state has not captured any funds. If a payment succeeds and you later refund it, Stripe returns the principal but keeps the transaction fee (2.9% + $0.30). The user sees no charge on their statement for a failed payment.

### Can I save cards for repeat purchases without PCI liability?

Yes. Create or retrieve a Stripe Customer object for each user and save their stripe_customer_id in your database. Stripe stores the card; your server only stores the customer ID. On subsequent purchases, pass the stripe_customer_id to payment_intent.create() and Stripe presents saved payment methods. You never touch raw card data, so PCI scope is minimal.

### How do refunds work — automatic or manual?

Refunds are manual by default — you call stripe.refunds.create() with the payment_intent_id or charge_id, optionally with an amount for partial refunds. Stripe then pushes a charge.refunded webhook event. Funds return to the customer's card in 5–10 business days. Build an admin screen to trigger refunds; do not expose the refund action to end users directly.

### What's the difference between Stripe Checkout and Stripe Elements?

Stripe Checkout is a Stripe-hosted page — fastest to implement, zero UI code, handles 3DS and Apple/Google Pay automatically, but limited visual customization. Stripe Elements embeds individual fields directly in your page, giving you full control over the layout and user experience. For most apps, Elements with the prebuilt Payment Element is the right balance: fully embedded, supports all payment methods, and handles 3DS without a redirect.

### Can I accept Apple Pay and Google Pay?

Yes, with no extra configuration when using Stripe Elements or the Payment Element — Stripe detects the browser and shows the wallet button automatically. Apple Pay requires your domain to be registered in the Stripe Dashboard (Stripe generates a verification file you serve from your domain). Google Pay works on any HTTPS page in Chrome on Android. Both require your Stripe account to be in live mode for real transactions.

### How do I handle 3D Secure for European cards?

Stripe handles 3DS automatically when you use stripe.confirmPayment() with the Payment Element. If the card requires authentication, Stripe triggers the 3DS modal without any extra code. For manual Payment Intent flows, listen for the requires_action status and call stripe.handleNextAction(clientSecret) — Stripe opens the bank's authentication iframe and resolves the promise when done.

---

Source: https://www.rapidevelopers.com/app-features/payment-gateway
© RapidDev — https://www.rapidevelopers.com/app-features/payment-gateway
