Webhooks
The Stripe events the app listens to, and how to test them locally.
Stripe tells the app when a payment or a subscription changes. The app listens at:
POST https://example.com/webhooks/stripe
It checks the Stripe-Signature header with the endpoint's signing secret (STRIPE_WEBHOOK_SECRET) and answers 400 when the signature does not match. It answers 200 for an event it handled or one it ignores, so Stripe stops sending it.
Events
| Event | What the app does |
|---|---|
checkout.session.completed | Starts the plan, or grants the credits of a pack once it is paid. |
checkout.session.async_payment_succeeded | The same, for a payment that cleared later. |
customer.subscription.created | Records the plan and its period. |
customer.subscription.updated | Records a change of plan, a cancellation or a renewal. |
customer.subscription.deleted | Moves the organization back to the free plan. |
invoice.paid | Clears a past-due state once Stripe says the subscription is paid. |
invoice.payment_failed | Starts the grace period and shows the payment notice. |
customer.tax_id.created, .updated, .deleted | Keeps the customer's VAT number. |
charge.refunded | Takes back the refunded share of a credit pack's credits. |
charge.dispute.created | Takes back the disputed credit pack's credits. |
Create the endpoint in the Stripe dashboard with these events selected. An event delivered twice is handled once.
Test locally
Run the Worker locally (bun run preview in apps/web, port 8790), then forward Stripe's test events to it with the Stripe CLI:
stripe listen --forward-to localhost:8790/webhooks/stripe
The CLI prints a signing secret (whsec_...). Put it in STRIPE_WEBHOOK_SECRET of your local variables, then trigger an event:
stripe trigger checkout.session.completed
Use test mode only. Never point a test endpoint at live keys.