Comprehensive guide for integrating Stripe with Firebase, Next.js, React, and TypeScript. Use when working with Stripe payments, subscriptions, webhooks, or checkout. Covers webhook setup with Firebase Cloud Functions v2, Next.js App Router webhook handling, signature verification, common errors and solutions, Stripe CLI usage, and MCP integration. Essential for setting up Stripe correctly in Firebase+Next.js+TypeScript projects.
Complete integration guide for Stripe with Firebase Cloud Functions v2, Next.js, React, TypeScript, and MUI v7.
This skill covers Stripe integration for projects using:
# Install Stripe Node SDK
npm install stripe
# Install types
npm install --save-dev @types/stripe
# Verify version (should be latest stable)
npm list stripe
Create/update .env.local (for Next.js):
# Stripe Keys
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_... # From Dashboard or CLI
# Firebase
NEXT_PUBLIC_FIREBASE_API_KEY=...
NEXT_PUBLIC_FIREBASE_PROJECT_ID=...
Create/update .secret.local (for Firebase Functions):
# Set secrets for Cloud Functions
firebase functions:secrets:set STRIPE_SECRET_KEY
firebase functions:secrets:set STRIPE_WEBHOOK_SECRET
Frontend (lib/stripe.ts):
import { loadStripe, Stripe } from '@stripe/stripe-js';
let stripePromise: Promise<Stripe | null>;
export const getStripe = () => {
if (!stripePromise) {
stripePromise = loadStripe(
process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!
);
}
return stripePromise;
};
Backend (lib/stripe-admin.ts):
import Stripe from 'stripe';
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: '2025-01-27.acacia', // Use latest stable version
typescript: true,
});
Webhooks are CRITICAL for Stripe integrations. Most Stripe events happen asynchronously.
Create functions/src/stripe-webhook.ts:
import { onRequest } from 'firebase-functions/v2/https';
import { logger } from 'firebase-functions';
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: '2025-01-27.acacia',
});
export const stripeWebhook = onRequest(
{
secrets: ['STRIPE_WEBHOOK_SECRET', 'STRIPE_SECRET_KEY'],
invoker: 'public', // Must be public for Stripe to call
cors: false, // No CORS needed for server-to-server
},
async (req, res) => {
// Verify method
if (req.method !== 'POST') {
res.status(405).send('Method Not Allowed');
return;
}
const signature = req.headers['stripe-signature'] as string;
if (!signature) {
res.status(400).send('Missing stripe-signature header');
return;
}
let event: Stripe.Event;
try {
// CRITICAL: Use req.rawBody for signature verification
event = stripe.webhooks.constructEvent(
req.rawBody!,
signature,
process.env.STRIPE_WEBHOOK_SECRET!
);
} catch (err) {
logger.error('Webhook signature verification failed:', err);
res.status(400).send(`Webhook Error: ${err.message}`);
return;
}
// IMPORTANT: Return 200 IMMEDIATELY
res.status(200).send({ received: true });
// Process event asynchronously AFTER responding
try {
await handleStripeEvent(event);
} catch (error) {
logger.error('Error processing webhook:', error);
// Don't throw - already responded to Stripe
}
}
);
async function handleStripeEvent(event: Stripe.Event) {
logger.info('Processing event:', event.type);
switch (event.type) {
case 'checkout.session.completed': {
const session = event.data.object as Stripe.Checkout.Session;
await handleCheckoutCompleted(session);
break;
}
case 'customer.subscription.created':
case 'customer.subscription.updated': {
const subscription = event.data.object as Stripe.Subscription;
await handleSubscriptionUpdate(subscription);
break;
}
case 'customer.subscription.deleted': {
const subscription = event.data.object as Stripe.Subscription;
await handleSubscriptionCanceled(subscription);
break;
}
case 'invoice.paid': {
const invoice = event.data.object as Stripe.Invoice;
await handleInvoicePaid(invoice);
break;
}
case 'invoice.payment_failed': {
const invoice = event.data.object as Stripe.Invoice;
await handlePaymentFailed(invoice);
break;
}
default:
logger.info('Unhandled event type:', event.type);
}
}
// Implement handlers
async function handleCheckoutCompleted(session: Stripe.Checkout.Session) {
// Grant access to product
logger.info('Checkout completed:', session.id);
}
async function handleSubscriptionUpdate(subscription: Stripe.Subscription) {
// Update subscription status in database
logger.info('Subscription updated:', subscription.id);
}
async function handleSubscriptionCanceled(subscription: Stripe.Subscription) {
// Revoke access
logger.info('Subscription canceled:', subscription.id);
}
async function handleInvoicePaid(invoice: Stripe.Invoice) {
// Confirm recurring payment
logger.info('Invoice paid:', invoice.id);
}
async function handlePaymentFailed(invoice: Stripe.Invoice) {
// Notify customer
logger.info('Payment failed:', invoice.id);
}
Create app/api/webhooks/stripe/route.ts:
import { NextRequest, NextResponse } from 'next/server';
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: '2025-01-27.acacia',
});
export async function POST(request: NextRequest) {
// CRITICAL: Get RAW body as text, not JSON
const body = await request.text();
const signature = request.headers.get('stripe-signature');
if (!signature) {
return NextResponse.json(
{ error: 'Missing stripe-signature header' },
{ 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:', err);
return NextResponse.json(
{ error: `Webhook Error: ${err.message}` },
{ status: 400 }
);
}
// Handle event
try {
switch (event.type) {
case 'checkout.session.completed':
// Handle checkout completion
break;
case 'customer.subscription.updated':
// Handle subscription update
break;
// Add more cases as needed
}
return NextResponse.json({ received: true });
} catch (error) {
console.error('Error processing webhook:', error);
return NextResponse.json(
{ error: 'Webhook processing failed' },
{ status: 500 }
);
}
}
Important: Next.js App Router handles body parsing correctly by default. Just use request.text().
# Deploy functions
firebase deploy --only functions:stripeWebhook
# Get function URL
firebase functions:config:get
# URL will be: https://us-central1-PROJECT_ID.cloudfunctions.net/stripeWebhook
whsec_)Essential tool for local development and testing.
Already installed if this message is seen. Verify with:
stripe --version
# Login to Stripe account
stripe login
# Generates pairing code and opens browser
# API key valid for 90 days
Most important CLI feature: Forward webhooks to local development server.
# Forward all events to local endpoint
stripe listen --forward-to http://localhost:3000/api/webhooks/stripe
# Output shows webhook signing secret:
# > Ready! Your webhook signing secret is 'whsec_xxxxx'
# Use this secret in .env.local for local development
In separate terminal, trigger test events:
# Trigger specific event
stripe trigger payment_intent.succeeded
# Trigger checkout completion
stripe trigger checkout.session.completed
# Trigger with custom data
stripe trigger checkout.session.completed \
--add checkout_session:client_reference_id=user_123
# Trigger subscription events
stripe trigger customer.subscription.created
stripe trigger customer.subscription.updated
stripe trigger customer.subscription.deleted
# View real-time API logs
stripe logs tail
# List products
stripe products list
# Create a product
stripe products create --name="Premium Plan" --description="Monthly subscription"
# Create a price
stripe prices create \
--product=prod_xxx \
--unit-amount=3990 \
--currency=brl \
--recurring[interval]=month
# Get customer details
stripe customers retrieve cus_xxx
# List recent charges
stripe charges list --limit=10
# Refund a payment
stripe refunds create --charge=ch_xxx
# List webhook endpoints
stripe webhook_endpoints list
MCP gives Claude direct access to Stripe data. Use for queries and operations.
The MCP is already configured with test credentials. Use these commands:
Query customers:
Please use the Stripe MCP to list my customers
Get payment information:
Use Stripe MCP to show my recent payment intents
Create resources:
Use Stripe MCP to create a new product called "Pro Plan"
Check subscription status:
Use Stripe MCP to check subscription status for customer cus_xxx
MCP is ideal for:
// Strongly type event data
switch (event.type) {
case 'checkout.session.completed': {
const session = event.data.object as Stripe.Checkout.Session;
// TypeScript now knows session properties
break;
}
case 'customer.subscription.updated': {
const subscription = event.data.object as Stripe.Subscription;
// Access subscription.status, subscription.items, etc.
break;
}
}
// When using expand parameter
const session = await stripe.checkout.sessions.retrieve(
sessionId,
{ expand: ['subscription', 'customer'] }
);
// Type assertion for expanded properties
const subscription = session.subscription as Stripe.Subscription;
const customer = session.customer as Stripe.Customer;
// Or type check
if (typeof session.subscription !== 'string') {
// session.subscription is Stripe.Subscription
}
// Define custom types for your application
interface UserSubscription {
stripeCustomerId: string;
stripeSubscriptionId: string;
status: Stripe.Subscription.Status;
priceId: string;
currentPeriodEnd: number;
}
import { stripe } from '@/lib/stripe-admin';
export async function createCheckoutSession(userId: string, priceId: string) {
const session = await stripe.checkout.sessions.create({
mode: 'subscription',
payment_method_types: ['card'],
line_items: [
{
price: priceId,
quantity: 1,
},
],
success_url: `${process.env.NEXT_PUBLIC_URL}/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.NEXT_PUBLIC_URL}/pricing`,
client_reference_id: userId, // Link to your user
metadata: {
userId,
},
});
return session;
}
// Get subscription
const subscription = await stripe.subscriptions.retrieve('sub_xxx');
// Update subscription
await stripe.subscriptions.update('sub_xxx', {
items: [{
id: subscription.items.data[0].id,
price: 'price_new',
}],
});
// Cancel subscription
await stripe.subscriptions.cancel('sub_xxx');
// Cancel at period end
await stripe.subscriptions.update('sub_xxx', {
cancel_at_period_end: true,
});
This skill includes detailed reference files:
webhook-events.md: Complete list of Stripe webhook events, when to use each, and handling best practicescommon-errors.md: Comprehensive error guide with solutions for signature verification, Firebase issues, TypeScript errors, and moreRead these files when: