Payment Integration with Bolt: A Complete Stripe Implementation Guide
Learn how to add real payment processing to your Bolt.new app. Step-by-step guide covering Stripe integration, webhooks, subscriptions, and security best practices.

Your Bolt prototype has a beautiful pricing page. Maybe it shows three tiers with sleek cards, a “Subscribe Now” button, and even a toggle between monthly and annual billing. The problem? None of it actually works. Click that button and nothing happens—or worse, you get a console error.
This is one of the most common gaps between a Bolt prototype and a production application. The UI looks ready to accept money, but there’s no actual payment infrastructure behind it. No Stripe account connected, no webhooks processing transactions, no database storing subscription states.
This guide walks through everything you need to know about adding real payment processing to your Bolt app. We’ll focus primarily on Stripe since it’s the most widely used payment processor for web applications, though the concepts apply broadly to other providers.
Why Payments Are Different From Other Features
Adding payments to your app isn’t like adding a new page or component. Money is involved, which means regulations, security requirements, and real consequences for mistakes. Here’s what makes payment integration unique:
The Stakes Are Higher
- • PCI DSS compliance requirements
- • Legal liability for data breaches
- • Chargebacks and fraud exposure
- • Tax collection obligations
- • Refund handling requirements
The Good News
- • Stripe handles most compliance for you
- • Never store card numbers directly
- • Well-documented APIs and SDKs
- • Test mode for safe development
- • Fraud detection built in
Choosing Your Payment Provider
Before diving into implementation, let’s compare the major options. Your choice affects everything from fees to development complexity.
| Provider | Transaction Fee | Best For | Developer Experience |
|---|---|---|---|
| Stripe | 2.9% + $0.30 | SaaS, subscriptions, marketplaces | Excellent docs, great SDK |
| PayPal | 2.99% + $0.49 | Consumer e-commerce, international | Older APIs, more complex |
| Square | 2.9% + $0.30 | Retail, in-person + online | Good, improving |
| Paddle | 5% + $0.50 | SaaS (handles taxes globally) | Simple but limited |
| Lemon Squeezy | 5% + $0.50 | Digital products, SaaS | Modern, developer-friendly |
For most Bolt apps, Stripe is the right choice. It has the best developer experience, handles the most complexity for you, and scales from your first customer to millions. The rest of this guide focuses on Stripe, though the architectural concepts apply to any provider.
Understanding Payment Architecture
Before writing any code, you need to understand how payments actually flow through your application. This architecture decision affects security, reliability, and your ability to handle edge cases.
Client-Side vs Server-Side: The Critical Distinction
Never Do This: Client-Side Only Payments
It might be tempting to handle everything in the browser, but this creates serious security vulnerabilities:
- • API keys exposed in client-side code
- • Prices can be modified by users
- • Payment confirmation can be spoofed
- • No reliable order fulfillment
The Right Way: Server-Side Payment Processing
Your server should handle all sensitive payment operations:
- • Create payment intents with validated prices
- • Store secret keys securely in environment variables
- • Process webhooks to confirm actual payments
- • Handle order fulfillment only after payment confirmation
The Payment Flow
Your frontend sends a request to your backend API
Your backend calls Stripe API with the correct price (from your database, not the client)
Frontend uses this to show Stripe’s secure payment form
Card info goes directly to Stripe—never touches your servers
This is the only reliable confirmation that payment succeeded
Update database, send confirmation email, provision access
Setting Up Stripe
Let’s walk through the actual implementation. We’ll assume you’re working with a Next.js app (which is what Bolt typically generates), but the concepts apply to any framework.
Step 1: Create Your Stripe Account
- 1. Go to stripe.com and create an account
- 2. Complete the business verification (required before going live)
- 3. Navigate to Developers → API keys
- 4. Copy your Publishable key (starts with pk_test_)
- 5. Copy your Secret key (starts with sk_test_)
Step 2: Environment Variables
Add these to your .env.local file:
# Stripe API Keys NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_your_key_here STRIPE_SECRET_KEY=sk_test_your_key_here STRIPE_WEBHOOK_SECRET=whsec_your_webhook_secret
Important: Only the publishable key should have NEXT_PUBLIC_ prefix. The secret key and webhook secret must never be exposed to the client.
Step 3: Install Dependencies
npm install stripe @stripe/stripe-js @stripe/react-stripe-js
Step 4: Create the Server-Side API Route
This API route creates PaymentIntents with prices you control:
// app/api/create-payment-intent/route.ts
import { NextResponse } from 'next/server';
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: '2024-11-20.acacia',
});
// Your product prices - stored server-side, not client
const PRICES = {
starter: 2900, // $29.00 in cents
pro: 9900, // $99.00 in cents
enterprise: 29900,
};
export async function POST(request: Request) {
try {
const { priceId, email } = await request.json();
// Validate the price exists
const amount = PRICES[priceId as keyof typeof PRICES];
if (!amount) {
return NextResponse.json(
{ error: 'Invalid price' },
{ status: 400 }
);
}
const paymentIntent = await stripe.paymentIntents.create({
amount,
currency: 'usd',
receipt_email: email,
metadata: {
priceId,
},
});
return NextResponse.json({
clientSecret: paymentIntent.client_secret,
});
} catch (error) {
console.error('Payment intent creation failed:', error);
return NextResponse.json(
{ error: 'Payment processing failed' },
{ status: 500 }
);
}
}Step 5: Build the Checkout Component
// components/CheckoutForm.tsx
'use client';
import { useState } from 'react';
import {
PaymentElement,
useStripe,
useElements,
} from '@stripe/react-stripe-js';
export default function CheckoutForm() {
const stripe = useStripe();
const elements = useElements();
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!stripe || !elements) return;
setLoading(true);
setError(null);
const { error: submitError } = await stripe.confirmPayment({
elements,
confirmParams: {
return_url: `${window.location.origin}/payment/success`,
},
});
if (submitError) {
setError(submitError.message || 'Payment failed');
setLoading(false);
}
// If successful, user is redirected to return_url
};
return (
<form onSubmit={handleSubmit} className="space-y-6">
<PaymentElement />
{error && (
<div className="text-red-600 text-sm">{error}</div>
)}
<button
type="submit"
disabled={!stripe || loading}
className="w-full bg-indigo-600 text-white py-3 rounded-lg
hover:bg-indigo-700 disabled:opacity-50"
>
{loading ? 'Processing...' : 'Pay Now'}
</button>
</form>
);
}Webhooks: The Most Critical Part
Here’s where many developers go wrong. They confirm payment based on what the client reports, which can be spoofed. The only reliable confirmation comes from Stripe webhooks.
Why Webhooks Matter
Common Mistake: Trusting client-side payment confirmation
When stripe.confirmPayment() succeeds on the client, it means the payment was submitted, not necessarily completed. The actual charge might still fail due to insufficient funds, fraud detection, or network issues. Always wait for the webhook.
Implementing the Webhook Handler
// app/api/webhooks/stripe/route.ts
import { NextResponse } from 'next/server';
import Stripe from 'stripe';
import { headers } from 'next/headers';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
export async function POST(request: Request) {
const body = await request.text();
const headersList = await headers();
const signature = headersList.get('stripe-signature');
if (!signature) {
return NextResponse.json(
{ error: 'Missing signature' },
{ status: 400 }
);
}
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(
body,
signature,
process.env.STRIPE_WEBHOOK_SECRET!
);
} catch (err) {
console.error('Webhook signature verification failed');
return NextResponse.json(
{ error: 'Invalid signature' },
{ status: 400 }
);
}
// Handle the event
switch (event.type) {
case 'payment_intent.succeeded':
const paymentIntent = event.data.object as Stripe.PaymentIntent;
await handleSuccessfulPayment(paymentIntent);
break;
case 'payment_intent.payment_failed':
const failedPayment = event.data.object as Stripe.PaymentIntent;
await handleFailedPayment(failedPayment);
break;
default:
console.log(`Unhandled event type: ${event.type}`);
}
return NextResponse.json({ received: true });
}
async function handleSuccessfulPayment(paymentIntent: Stripe.PaymentIntent) {
const { priceId } = paymentIntent.metadata;
const email = paymentIntent.receipt_email;
// This is where you:
// 1. Update your database
// 2. Provision access to your product
// 3. Send confirmation email
// 4. Trigger any other business logic
console.log(`Payment succeeded for ${email}, plan: ${priceId}`);
}
async function handleFailedPayment(paymentIntent: Stripe.PaymentIntent) {
// Log the failure, notify your team, maybe email the customer
console.error(`Payment failed: ${paymentIntent.id}`);
}Testing Webhooks Locally
Stripe can’t send webhooks to localhost, so you’ll need the Stripe CLI:
# Install Stripe CLI brew install stripe/stripe-cli/stripe # Login to your Stripe account stripe login # Forward webhooks to your local server stripe listen --forward-to localhost:3000/api/webhooks/stripe # The CLI will show you the webhook signing secret to use locally
Handling Subscriptions
If you’re building a SaaS, you’ll need recurring payments. Stripe handles this through Subscriptions, which work differently from one-time payments.
Setting Up Products and Prices
First, create your subscription products in the Stripe Dashboard (or via API):
- 1. Go to Products in your Stripe Dashboard
- 2. Create a new product (e.g., “Pro Plan”)
- 3. Add a recurring price (e.g., $29/month)
- 4. Copy the Price ID (starts with price_)
Creating a Subscription Checkout
// app/api/create-checkout-session/route.ts
import { NextResponse } from 'next/server';
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
export async function POST(request: Request) {
try {
const { priceId, customerId } = await request.json();
const session = await stripe.checkout.sessions.create({
mode: 'subscription',
payment_method_types: ['card'],
line_items: [
{
price: priceId, // Your Stripe Price ID
quantity: 1,
},
],
customer: customerId, // Optional: existing Stripe customer
success_url: `${process.env.NEXT_PUBLIC_URL}/dashboard?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.NEXT_PUBLIC_URL}/pricing`,
});
return NextResponse.json({ url: session.url });
} catch (error) {
console.error('Checkout session creation failed:', error);
return NextResponse.json(
{ error: 'Failed to create checkout' },
{ status: 500 }
);
}
}Subscription Webhook Events
Subscriptions have their own lifecycle events you need to handle:
customer.subscription.created
New subscription started. Grant access to your product.
invoice.paid
Recurring payment succeeded. Extend access period.
invoice.payment_failed
Payment failed. Notify customer, maybe implement grace period.
customer.subscription.deleted
Subscription ended. Revoke access.
Security Best Practices
Payment security isn’t optional. Here are the non-negotiable practices you must follow:
1. Never Log Sensitive Data
Don’t log full card numbers, CVVs, or API secrets. Use Stripe’s dashboard for transaction details.
2. Always Verify Webhook Signatures
Never skip signature verification. It’s your only guarantee the webhook came from Stripe.
3. Validate Prices Server-Side
Never trust prices from the client. Look up prices from your database or Stripe.
4. Handle Idempotency
Webhooks can be sent multiple times. Make sure processing the same event twice doesn’t double-charge or double-provision.
5. Use Test Mode Until Ready
Stripe’s test mode is identical to production. Test thoroughly before switching to live keys.
Testing Your Payment Flow
Stripe provides test card numbers for every scenario you need to handle:
| Card Number | Scenario |
|---|---|
| 4242 4242 4242 4242 | Successful payment |
| 4000 0000 0000 0002 | Card declined |
| 4000 0000 0000 9995 | Insufficient funds |
| 4000 0025 0000 3155 | Requires 3D Secure |
Use any future expiration date and any 3-digit CVC. Test every error scenario before going live.
Common Pitfalls When Moving to Production
Pitfall: Forgetting to Switch Keys
Using test keys in production means no real payments are processed. Using live keys in development means real charges.
Prevention: Use environment variables and double-check before deploying.
Pitfall: Not Handling Failed Payments Gracefully
Users get confused if payment fails without clear feedback. They might try multiple times, leading to frustration.
Prevention: Display clear error messages and suggest next steps.
Pitfall: Ignoring Webhook Failures
If your webhook endpoint goes down, Stripe will retry, but you might miss events and fail to fulfill orders.
Prevention: Monitor webhook health in Stripe Dashboard. Set up alerting for failures.
Pitfall: Not Storing Transaction References
Without storing PaymentIntent IDs, you can’t issue refunds or investigate disputes.
Prevention: Always store Stripe IDs in your database alongside your order records.
Next Steps
You now understand the fundamentals of adding payments to your Bolt app. The key takeaways:
- • Always process payments server-side with secret keys stored securely
- • Webhooks are your source of truth—never trust client-side confirmation
- • Test thoroughly with Stripe’s test cards before going live
- • Handle errors gracefully with clear user feedback
- • Store transaction references for refunds and disputes
Payment integration is one of the more complex parts of taking a Bolt prototype to production, but it’s also what transforms your prototype into a real business. Take the time to implement it correctly, and you’ll have a solid foundation for monetizing your application.
Need Help With Your Bolt Payment Integration?
If implementing payments feels overwhelming, or you want to ensure it’s done right the first time, we can help. Our team has integrated payments for dozens of Bolt apps.
Schedule a CallReady to Build Your MVP?
Need help turning your idea into reality? Our team has built 50+ successful startup MVPs and knows exactly what it takes to validate your idea quickly and cost-effectively.