Blog / AI Tool Development

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.

ShipAi Team
16 min read
Payment Integration with Bolt: A Complete Stripe Implementation Guide

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.

ProviderTransaction FeeBest ForDeveloper Experience
Stripe2.9% + $0.30SaaS, subscriptions, marketplacesExcellent docs, great SDK
PayPal2.99% + $0.49Consumer e-commerce, internationalOlder APIs, more complex
Square2.9% + $0.30Retail, in-person + onlineGood, improving
Paddle5% + $0.50SaaS (handles taxes globally)Simple but limited
Lemon Squeezy5% + $0.50Digital products, SaaSModern, 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

1
Customer clicks “Buy”

Your frontend sends a request to your backend API

2
Server creates PaymentIntent

Your backend calls Stripe API with the correct price (from your database, not the client)

3
Client receives client_secret

Frontend uses this to show Stripe’s secure payment form

4
Customer enters payment details

Card info goes directly to Stripe—never touches your servers

5
Stripe sends webhook to your server

This is the only reliable confirmation that payment succeeded

6
Server fulfills the order

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. 1. Go to stripe.com and create an account
  2. 2. Complete the business verification (required before going live)
  3. 3. Navigate to Developers → API keys
  4. 4. Copy your Publishable key (starts with pk_test_)
  5. 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. 1. Go to Products in your Stripe Dashboard
  2. 2. Create a new product (e.g., “Pro Plan”)
  3. 3. Add a recurring price (e.g., $29/month)
  4. 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 NumberScenario
4242 4242 4242 4242Successful payment
4000 0000 0000 0002Card declined
4000 0000 0000 9995Insufficient funds
4000 0025 0000 3155Requires 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 Call

Ready 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.