Integrating Stripe Webhooks
Overview
Stripe webhooks require raw request bodies for signature verification. Most web frameworks parse JSON automatically, breaking verification. This skill provides framework-specific solutions for the raw body problem and documents common TypeScript type mismatches.
When to Use
Use this skill when:
-
Getting "Raw body not available" errors from Stripe webhooks
-
Webhook signature verification fails with 400 errors
-
Implementing new Stripe webhook endpoints
-
Getting TypeError: Cannot read property 'current_period_start' from subscription events
-
Webhooks return 404 (route registration issues)
Don't use for:
-
General Stripe API integration (not webhooks)
-
Frontend Stripe Elements implementation
-
Stripe checkout session creation (use Stripe docs)
Quick Reference
Problem Solution
Raw body not available Configure custom body parser (see framework examples)
Signature verification fails Use raw body bytes/buffer, not parsed JSON
404 on webhook endpoint Register webhook route inside API prefix
current_period_start undefined Access from subscription.items.data[0] not root
URI validation errors URL-encode dynamic parameters with encodeURIComponent()
Critical: Raw Body Parsing
THE PROBLEM: Stripe's constructEvent() requires the exact bytes received to verify the signature. JSON parsing modifies the body, breaking verification.
THE SOLUTION: Access raw body before any parsing middleware.
Framework Examples
Node.js - Fastify (most common for new projects):
// In main server file, BEFORE registering routes server.addContentTypeParser('application/json', { parseAs: 'buffer' }, async (req: any, body: Buffer) => { req.rawBody = body; // Store for webhooks return JSON.parse(body.toString('utf8')); // Parse for other routes } );
// In webhook handler const rawBody = (request as any).rawBody; const event = stripe.webhooks.constructEvent( rawBody, signature, webhookSecret );
Node.js - Express:
// Define webhook route BEFORE express.json() middleware app.post('/webhooks/stripe', express.raw({ type: 'application/json' }), (req, res) => { const event = stripe.webhooks.constructEvent( req.body, // Already raw Buffer req.headers['stripe-signature'], webhookSecret ); } );
app.use(express.json()); // After webhook route
Python - FastAPI:
@app.post('/webhooks/stripe') async def stripe_webhook(request: Request): payload = await request.body() # Use .body() not .json() signature = request.headers.get('stripe-signature')
event = stripe.Webhook.construct_event(
payload, signature, webhook_secret
)
General Pattern: Get raw bytes/buffer → verify signature → use parsed event from Stripe.
Common Mistakes
- Subscription Period Fields Missing
Error: TypeError: Cannot read property 'current_period_start' of undefined
Cause: Stripe returns period dates in subscription.items.data[0] , not at subscription root. TypeScript types don't include these fields on SubscriptionItem .
Fix:
// ❌ WRONG - fields don't exist here new Date(subscription.current_period_start * 1000)
// ✅ CORRECT - get from first subscription item const firstItem = subscription.items.data[0] as any; const periodStart = firstItem?.current_period_start || subscription.billing_cycle_anchor; const periodEnd = firstItem?.current_period_end || subscription.billing_cycle_anchor;
await updateOrg({ start_date: new Date(periodStart * 1000), end_date: new Date(periodEnd * 1000), });
- Route Not Found (404)
Cause: Webhook routes registered outside API prefix.
// ❌ WRONG - creates /webhooks/stripe instead of /api/v1/webhooks/stripe export async function registerRoutes(server) { server.register(async (api) => { await api.register(subscriptionRoutes, { prefix: '/subscriptions' }); }, { prefix: '/api/v1' });
await server.register(webhookRoutes, { prefix: '/webhooks' }); // Outside! }
// ✅ CORRECT - inside API prefix export async function registerRoutes(server) { server.register(async (api) => { await api.register(subscriptionRoutes, { prefix: '/subscriptions' }); await api.register(webhookRoutes, { prefix: '/webhooks' }); // Inside }, { prefix: '/api/v1' }); }
- URL Encoding in Checkout URLs
Error: "body/successUrl must match format 'uri'"
Cause: Organization names or parameters with spaces not URL-encoded.
// ❌ WRONG - "Broke Org" creates invalid URL
const successUrl = ${origin}/orgs?name=${orgName}&subscription=success;
// ✅ CORRECT - encode dynamic parameters
const successUrl = ${origin}/orgs?name=${encodeURIComponent(orgName)}&subscription=success;
Implementation Checklist
Server Setup:
-
Configure raw body parser BEFORE routes
-
Register webhook routes inside API prefix (if using one)
-
Set STRIPE_WEBHOOK_SECRET environment variable
-
Verify webhook secret is configured before processing
Webhook Handler:
-
Validate stripe-signature header exists
-
Access raw body (not parsed JSON)
-
Use stripe.webhooks.constructEvent() for verification
-
Handle SignatureVerificationError separately
-
Return 200 for received events (even if processing fails)
-
Log all events with ID and type
Subscription Events:
-
Get period dates from subscription.items.data[0]
-
Cast to any to access TypeScript-missing fields
-
Fallback to billing_cycle_anchor if items missing
-
Store org_id in subscription metadata
-
Update verification status based on subscription status
Frontend:
-
URL-encode all dynamic parameters
-
URL-encode organization names in success/cancel URLs
-
Handle checkout errors gracefully
-
Poll for verification after checkout success
Testing Locally
Install Stripe CLI
brew install stripe/stripe-cli/stripe
Forward webhooks to local server
stripe listen --forward-to localhost:3000/api/v1/webhooks/stripe
Trigger test events
stripe trigger customer.subscription.created stripe trigger customer.subscription.updated stripe trigger invoice.paid
Real-World Impact
Before applying these patterns:
-
Webhooks fail with 400 "Invalid signature"
-
Subscription updates crash with undefined property errors
-
Hours debugging TypeScript type mismatches
-
Checkout fails with URL validation errors
After applying:
-
Webhooks verify successfully
-
Subscription data extracts correctly
-
Type-safe with explicit casting
-
Checkout URLs work with any organization name
References
-
Stripe Webhook Signature Verification
-
Stripe Subscription Object
-
See framework documentation for body parsing middleware