Webhooks
Understand how Stripe webhooks work in Vibestacks. Learn about automatic event handling, custom event processing, and webhook security.
Webhooks are how Stripe notifies your app about events like successful payments, subscription changes, and failed charges. Vibestacks handles the most common events automatically.
How Webhooks Work
┌─────────┐ ┌────────────────────┐ ┌──────────────┐
│ Stripe │ ──────> │ /api/auth/stripe/ │ ──────> │ Better Auth │
│ │ POST │ webhook │ │ Plugin │
└─────────┘ └────────────────────┘ └──────────────┘
│
▼
┌──────────────┐
│ Database │
│ (subscription│
│ updated) │
└──────────────┘- An event occurs in Stripe (payment, subscription change, etc.)
- Stripe sends a POST request to your webhook endpoint
- Better Auth verifies the signature and processes the event
- Your database is updated accordingly
Webhook Endpoint
The webhook endpoint is automatically created by Better Auth at:
POST /api/auth/stripe/webhookDon't Create Your Own
You don't need to create a webhook route manually. Better Auth's Stripe plugin handles this automatically.
Automatically Handled Events
Better Auth processes these events out of the box:
| Event | What Happens |
|---|---|
checkout.session.completed | Creates subscription in database |
customer.subscription.created | Creates subscription (if not via checkout) |
customer.subscription.updated | Updates subscription status, dates, plan |
customer.subscription.deleted | Marks subscription as canceled |
These events keep your database in sync with Stripe automatically.
Required Events
When setting up your Stripe webhook endpoint, select at least these events:
checkout.session.completed
customer.subscription.created
customer.subscription.updated
customer.subscription.deletedRecommended Additional Events
For better monitoring and features:
invoice.paid
invoice.payment_failed
customer.subscription.trial_will_endCustom Event Handling
Handle additional events using the onEvent callback in lib/auth.ts:
stripe({
stripeClient,
stripeWebhookSecret: process.env.STRIPE_WEBHOOK_SECRET!,
// Handle any Stripe event
onEvent: async (event) => {
switch (event.type) {
case "invoice.paid":
// Payment successful
console.log(`Invoice paid: ${event.data.object.id}`);
break;
case "invoice.payment_failed":
// Payment failed - maybe send an email
const invoice = event.data.object;
await sendPaymentFailedEmail({
customerId: invoice.customer as string,
amount: invoice.amount_due,
});
break;
case "customer.subscription.trial_will_end":
// Trial ending in 3 days
await sendTrialEndingReminder({
subscriptionId: event.data.object.id,
});
break;
}
},
subscription: {
// ...
},
})Webhook Security
Stripe signs every webhook request. Better Auth automatically:
- Verifies the signature using your
STRIPE_WEBHOOK_SECRET - Rejects requests with invalid signatures
- Prevents replay attacks
Keep Your Secret Safe
Never expose your STRIPE_WEBHOOK_SECRET. It's used to verify that requests actually come from Stripe.
Signature Verification
If you see this error:
Webhook signature verification failedCommon causes:
- Wrong
STRIPE_WEBHOOK_SECRET(test vs live, or stale from CLI) - Request body was modified by middleware
- Clock skew between servers
Testing Webhooks Locally
Use the Stripe CLI to forward webhooks to localhost:
# Start forwarding
stripe listen --forward-to localhost:3000/api/auth/stripe/webhook
# In another terminal, trigger a test event
stripe trigger checkout.session.completedUseful Test Events
# Subscription lifecycle
stripe trigger customer.subscription.created
stripe trigger customer.subscription.updated
stripe trigger customer.subscription.deleted
# Payments
stripe trigger invoice.paid
stripe trigger invoice.payment_failed
# Trials
stripe trigger customer.subscription.trial_will_endViewing Webhook Logs
Stripe CLI (Local)
When running stripe listen, you'll see real-time logs:
2024-01-15 10:30:45 --> checkout.session.completed [evt_xxx]
2024-01-15 10:30:45 <-- [200] POST http://localhost:3000/api/auth/stripe/webhookStripe Dashboard (Production)
Go to dashboard.stripe.com/webhooks:
- Click on your endpoint
- View Webhook attempts tab
- See status, response, and payload for each event
Retry Behavior
If your endpoint returns an error (non-2xx status), Stripe will retry:
| Attempt | Delay |
|---|---|
| 1 | Immediate |
| 2 | 5 minutes |
| 3 | 30 minutes |
| 4 | 2 hours |
| 5+ | 8 hours (up to 3 days) |
After 3 days of failures, Stripe stops retrying and marks the event as failed.
Idempotency
Always design your webhook handlers to be idempotent. The same event might be delivered multiple times due to retries.
Common Issues
400 Bad Request
- Webhook secret doesn't match
- Request body parsing issue (ensure raw body is preserved)
401 Unauthorized
- Missing webhook secret
- Secret doesn't match the endpoint
Timeout (504)
Your handler is taking too long. Webhooks should respond within 30 seconds.
Solution: Process heavy work in the background:
onEvent: async (event) => {
// Don't await heavy operations
// Use a queue or background job instead
waitUntil(processHeavyTask(event));
}Events Not Arriving
- Check Stripe Dashboard → Webhooks → Endpoint → Recent attempts
- Verify the endpoint URL is correct
- Ensure your server is accessible (not behind firewall)
- Check that you selected the correct events
Event Data Types
Each event has a typed data.object. Common shapes:
// checkout.session.completed
event.data.object: Stripe.Checkout.Session
// customer.subscription.*
event.data.object: Stripe.Subscription
// invoice.*
event.data.object: Stripe.InvoiceAccess typed data:
onEvent: async (event) => {
if (event.type === "invoice.paid") {
const invoice = event.data.object as Stripe.Invoice;
console.log(invoice.amount_paid);
console.log(invoice.customer);
}
}Next Steps
- Lifecycle Emails - Send emails on subscription events
- Billing Portal - Let users manage subscriptions
Free Trials
Configure free trial periods for your subscription plans. Learn how trials work, prevent abuse, and handle trial lifecycle events.
Lifecycle Emails
Send automated emails for subscription events like welcome messages, trial reminders, and cancellation confirmations. Learn how to set up and customize subscription lifecycle emails.