--- name: integrating-stripe-webhooks description: Use when implementing Stripe webhook endpoints and getting 'Raw body not available' or signature verification errors - provides raw body parsing solutions and subscription period field fixes across frameworks --- # 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): ```typescript // 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**: ```javascript // 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**: ```python @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 ### 1. 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:** ```typescript // ❌ 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), }); ``` ### 2. Route Not Found (404) **Cause:** Webhook routes registered outside API prefix. ```typescript // ❌ 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' }); } ``` ### 3. URL Encoding in Checkout URLs **Error:** `"body/successUrl must match format 'uri'"` **Cause:** Organization names or parameters with spaces not URL-encoded. ```typescript // ❌ 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 ```bash # 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](https://stripe.com/docs/webhooks/signatures) - [Stripe Subscription Object](https://stripe.com/docs/api/subscriptions/object) - See framework documentation for body parsing middleware