Webhooks
Stripe needs to reach the service at POST /webhooks/stripe to sync payment,
subscription and invoice state. The endpoint doesn’t use the API key; it
verifies Stripe’s signature on the raw request body instead.
Local development
Section titled “Local development”Use the Stripe CLI:
stripe listen --forward-to localhost:3000/webhooks/stripeCopy the printed whsec_... into .env as STRIPE_WEBHOOK_SECRET and restart
the service. Keep stripe listen running while you develop.
Production
Section titled “Production”- In the Stripe dashboard, open Developers → Webhooks and add an endpoint
pointing at
https://your-host/webhooks/stripe. - Subscribe it to
payment_intent.*,customer.subscription.*andinvoice.*events. - Set that endpoint’s signing secret as
STRIPE_WEBHOOK_SECRET.
Handled events
Section titled “Handled events”| Events | Effect |
|---|---|
payment_intent.succeeded, payment_intent.payment_failed, payment_intent.canceled |
Sync the local payment status |
customer.subscription.created, .updated, .deleted |
Sync subscription status, billing period and trial end |
invoice.paid, invoice.payment_failed, invoice.finalized |
Create or update the local invoice |
Any other event type is recorded and acknowledged with no further action.
How delivery is processed
Section titled “How delivery is processed”- Verify. The signature is checked against the raw body. An invalid
signature returns
400. - Deduplicate. The event ID is inserted into
webhook_eventswithON CONFLICT DO NOTHING. Concurrent deliveries of the same event race on the insert and exactly one wins. - Enqueue. The event goes onto a BullMQ queue, with the event ID as the job ID as a second layer of dedup.
- Acknowledge. The endpoint returns
200right away, well inside Stripe’s timeout. - Process. A background worker applies the change. A failed job is
retried 5 times with exponential backoff. Jobs that exhaust their retries
stay in Redis for inspection, and the
webhook_eventsrow keeps the last error.