Skip to main content
RapidDev - Software Development Agency
App Featurespayments-commerce17 min read

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

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.

4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members

Feature spec

Intermediate

Category

payments-commerce

Build with AI

3–6 hours with Lovable or V0

Custom build

1–2 weeks custom dev

Running cost

$0 until first charge; then 2.9% + $0.30 per transaction

Works on

WebMobile

Everything it takes to ship a Payment Gateway — parts, prompts, and real costs.

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.

What users consider table stakes in 2026

  • Stripe Elements or Stripe Checkout embedded on the page — no unexpected full-page redirects before the user decides to pay
  • Apple Pay and Google Pay one-tap buttons visible on mobile browsers and Safari without any extra configuration
  • Real-time card validation feedback as the user types — card number, expiry, and CVC errors shown inline before the submit button is pressed
  • Clear, human-readable error messages for declined cards with a specific retry prompt (not just 'payment failed')
  • Order confirmation screen shown immediately after payment with a receipt email arriving within 60 seconds
  • Payment history accessible inside the user's account settings showing amount, date, status, and a link to the Stripe receipt

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.

Layers:UIDataBackend

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.

Note: Stripe Elements requires HTTPS and will not initialize on http://. Test on your published URL, not localhost or a Lovable preview iframe.

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.

Note: Never create Payment Intents client-side — amount and currency can be manipulated. Always server-side.

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.

Note: This is the most failure-prone component. Read the raw body as Uint8Array before any JSON parsing — parsing first mangles the signature and breaks verification.

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.

Note: Add a UNIQUE index on stripe_payment_intent_id — prevents duplicate fulfillment if your webhook handler is invoked twice for the same event.

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.

Note: Trigger email from the webhook, not the success URL redirect. Client can disappear; webhook always fires.

The data model

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

schema.sql
1create table public.orders (
2 id uuid primary key default gen_random_uuid(),
3 user_id uuid references auth.users(id) on delete set null,
4 stripe_payment_intent_id text unique not null,
5 stripe_customer_id text,
6 amount_cents integer not null check (amount_cents >= 50),
7 currency text not null default 'usd',
8 status text not null default 'pending'
9 check (status in ('pending', 'paid', 'failed', 'refunded')),
10 metadata jsonb default '{}',
11 created_at timestamptz not null default now(),
12 updated_at timestamptz not null default now()
13);
14
15alter table public.orders enable row level security;
16
17create policy "Users can view own orders"
18 on public.orders for select
19 using (auth.uid() = user_id);
20
21create table public.stripe_customers (
22 id uuid primary key default gen_random_uuid(),
23 user_id uuid references auth.users(id) on delete cascade not null unique,
24 stripe_customer_id text not null unique,
25 created_at timestamptz not null default now()
26);
27
28alter table public.stripe_customers enable row level security;
29
30create policy "Users can view own Stripe customer record"
31 on public.stripe_customers for select
32 using (auth.uid() = user_id);
33
34create index orders_user_id_created_idx
35 on public.orders (user_id, created_at desc);
36
37create index orders_payment_intent_idx
38 on public.orders (stripe_payment_intent_id);

Heads up: 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 it — pick your path

Each tool fits this feature differently. Switch tabs to compare fit, time, the exact steps, and a copy-paste prompt.

Full-stack, non-technical friendlyFit for this feature:

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.

Step by step

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

Where this path bites

  • 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

Third-party services you'll need

Three services cover the full payment stack. Stripe is mandatory; Resend and Supabase scale gracefully on free tiers for early-stage products.

ServiceWhat it doesFree tierPaid from
StripeCore payment processing: card acceptance, Apple/Google Pay, 3DS, fraud detection (Radar), webhooksFree to sign up; test mode unlimited2.9% + $0.30 per charge; Radar fraud: $0.05/charge (approx); Stripe Billing adds 0.5–0.8% for subscriptions
ResendTransactional receipt emails triggered by the payment_intent.succeeded webhook100 emails/day free$20/mo (50K emails) (approx)
SupabasePostgreSQL orders table + RLS + Edge Functions for Payment Intent creation and webhook handlingFree (2 projects)Pro $25/mo

Swipe the table sideways to see pricing.

What it costs to run

Drag through the tiers to see how your monthly bill scales with users — no surprises later.

Estimated monthly running cost

$10/mo

Stripe fees on purchase volume ($3–10 depending on average order value) + Supabase free tier + Resend free tier. Negligible fixed cost.

Estimates use verified 2026 pricing and assume typical usage per user. Your real bill depends on activity, storage, and third-party plans.

What breaks when AI tools build this

The failures people actually hit on their first build — the symptom, why it happens, and the exact fix.

Webhook fires before the browser redirect completes — double fulfillment

Symptom: 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

Symptom: 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

Symptom: 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

Symptom: 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

Symptom: 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

1

Always create Payment Intents server-side — never expose your Stripe secret key to the browser or allow amounts to be set client-side

2

Use Stripe's idempotency keys on Payment Intent creation so retried requests return the same intent instead of charging twice

3

Make your webhook handler idempotent: check the order's current status before updating, and use INSERT ... ON CONFLICT DO NOTHING for receipt records

4

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

5

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

6

Trigger receipt emails from the webhook handler, not the success URL redirect — the client can close the tab before the redirect fires

7

Display proration previews before plan upgrades using stripe.invoices.retrieveUpcoming() — surprise charges at upgrade are the top cause of subscription cancellations

8

Scaffold two separate Stripe API key secrets (test and live) from the beginning; retrofitting this after launch is painful and error-prone

When You Need Custom Development

AI tools handle standard checkout well. A few payment architectures genuinely require custom development:

  • Multi-vendor marketplace where each seller receives a separate payout via Stripe Connect — the split payment logic, platform fee calculation, and seller onboarding flow require bespoke Edge Function design
  • Volume-based or seat-based pricing with proration — Stripe Billing's schedule phases and pending invoice items need careful custom implementation to behave predictably
  • PCI DSS compliance audit required by an enterprise client — this mandates architecture review and documentation AI tools cannot produce
  • Complex partial refund workflows with a role-based approval chain before the Stripe refund API is called

RapidDev builds these features for production

Real auth, database, and payments — fixed price, delivered in weeks. You own 100% of the code.

Get a fixed-price quote

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.

RapidDev

Need this feature production-ready?

RapidDev builds a payment gateway into real apps — auth, database, payments — at $13K–$25K.

Book a free consultation

30-min call. No commitment.

Want this built for you?

We ship production apps at a fixed price — $13K–$25K, 6–10 weeks, source code yours. You've seen what it takes; we do it every week.

Get a fixed-price quote

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.