Webhooks are CRITICAL for Stripe integrations. Most Stripe events happen asynchronously.
Firebase Cloud Functions v2 Webhook Handler
Create functions/src/stripe-webhook.ts:
```typescript
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);
}
```
Next.js App Router Webhook Handler
Create app/api/webhooks/stripe/route.ts:
```typescript
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().
Deploying Webhook Endpoint
#### Firebase Deployment
```bash
# 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
```
#### Configure in Stripe Dashboard
- Go to https://dashboard.stripe.com/test/webhooks
- Click "Add endpoint"
- Enter your webhook URL
- Select events to listen for (or select "Send all events" for development)
- Click "Add endpoint"
- Copy webhook signing secret (starts with
whsec_) - Add secret to environment variables