Skip to main content

Webhook Routes

Stripe webhook handler and outbound webhooks.

Not Polar

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

EventWhat it does
checkout.session.completedMark payment paid, upgrade the related project / user
invoice.paidActivate recurring promotions / sponsorships
customer.subscription.updatedSync subscription status
customer.subscription.deletedRevoke promo placements when subscription ends

Security

Every request is verified with stripe.webhooks.constructEvent():

  1. Read the raw request body (important: not req.json() — signature fails)
  2. Read the stripe-signature header
  3. 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

stripe listen --forward-to localhost:3000/api/webhooks/stripe

The CLI prints a local webhook secret (whsec_...). Paste that into .env.local temporarily for local work.

Trigger a specific event:

stripe trigger checkout.session.completed

Option B — built-in simulator

pnpm webhook:simulate

Runs 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.

See also