integrating-stripe-webhooks
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
What this skill does
# 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
Related in Sales & CRM
process-mapper
IncludedUse when a BizOps lead, COO, or process-improvement owner needs to document an end-to-end business process (procurement, employee onboarding, incident handoff, customer-onboarding, claims adjudication) in BPMN-style notation, measure cycle times by stage, surface where work spends most of its time waiting vs. being worked, and quantify the gap between processing time and total elapsed time. Pairs Lean / Six Sigma / Theory-of-Constraints canon with deterministic stdlib-only Python tools to produce a process map, a ranked bottleneck list (with severity + root-cause hypothesis), and a cycle-time analysis (P50, P90, value-add ratio, Little's-Law throughput). Distinct from sales-pipeline, system-reliability (SLO), and strategic-OKR work — this is tactical process documentation for internal operations.
payment-integration
IncludedIntegrate payments with SePay (VietQR), Polar, Stripe, Paddle (MoR subscriptions), Creem.io (licensing). Checkout, webhooks, subscriptions, QR codes, multi-provider orders.
customer-success-manager
IncludedMonitors customer health, predicts churn risk, and identifies expansion opportunities using weighted scoring models for SaaS customer success
sales-engineer
IncludedAnalyzes RFP/RFI responses for coverage gaps, builds competitive feature comparison matrices, and plans proof-of-concept (POC) engagements for pre-sales engineering. Use when responding to RFPs, bids, or proposal requests; comparing product features against competitors; planning or scoring a customer POC or sales demo; preparing a technical proposal; or performing win/loss competitor analysis. Handles tasks described as 'RFP response', 'bid response', 'proposal response', 'competitor comparison', 'feature matrix', 'POC planning', 'sales demo prep', or 'pre-sales engineering'.
customer-success-manager
IncludedMonitors customer health, predicts churn risk, and identifies expansion opportunities using weighted scoring models for SaaS customer success
sales-engineer
IncludedAnalyzes RFP/RFI responses for coverage gaps, builds competitive feature comparison matrices, and plans proof-of-concept (POC) engagements for pre-sales engineering. Use when responding to RFPs, bids, or proposal requests; comparing product features against competitors; planning or scoring a customer POC or sales demo; preparing a technical proposal; or performing win/loss competitor analysis. Handles tasks described as 'RFP response', 'bid response', 'proposal response', 'competitor comparison', 'feature matrix', 'POC planning', 'sales demo prep', or 'pre-sales engineering'.