Webhook Routes
Stripe webhook handler and outbound webhooks.
Older versions of this boilerplate targeted Polar. The current webhook is Stripe-only. If your deploy still has a /api/webhooks/polar route, it's legacy.
POST /api/webhooks/stripe
Receives events from Stripe. Confirms payments, activates premium plans, fires email + outbound webhook notifications.
Excluded from middleware so session cookies aren't touched during the signature check.
Events handled
| Event | What it does |
|---|---|
checkout.session.completed | Mark payment paid, upgrade the related project / user |
invoice.paid | Activate recurring promotions / sponsorships |
customer.subscription.updated | Sync subscription status |
customer.subscription.deleted | Revoke promo placements when subscription ends |
Security
Every request is verified with stripe.webhooks.constructEvent():
- Read the raw request body (important: not
req.json()— signature fails) - Read the
stripe-signatureheader - Verify with
STRIPE_WEBHOOK_SECRET
If verification fails → respond 400. Legitimate Stripe requests respond 200.
Setup
See Payments for the Stripe webhook setup checklist.
Testing locally
Option A — Stripe CLI (recommended)
stripe listen --forward-to localhost:3000/api/webhooks/stripeThe CLI prints a local webhook secret (whsec_...). Paste that into .env.local temporarily for local work.
Trigger a specific event:
stripe trigger checkout.session.completedOption B — built-in simulator
pnpm webhook:simulateRuns scripts/simulate-webhook.js — fakes a Stripe event payload locally. Useful when you just want to run the handler once without a full Stripe account.
Outbound webhooks
DirectoryKit can send webhooks too — typically to Discord or Slack. Not a route, a helper:
import { webhookEvents } from '@/lib/webhooks';
await webhookEvents.projectSubmitted(project);
await webhookEvents.projectApproved(project);
await webhookEvents.projectRejected(project);
await webhookEvents.userRegistered(user);Configure the endpoints in the admin panel → Settings → Integrations (stored in the external_webhooks table). Disable the system entirely with webhooksExternal: false in features.