Webhooks
Billing providers notify the backend of payment events via webhooks. All webhook endpoints are registered in packages/backend/convex/http.ts.
Billing providers notify the backend of payment events via webhooks. The active
provider's integration registers its own single webhook route from
packages/backend/convex/http.ts (resolved from BILLING_PROVIDER via
billing/providers.ts). With BILLING_PROVIDER unset, no billing webhook
route exists — there is no public endpoint to receive unsigned traffic, rather
than a route guarded only by a secret check.
Webhook Endpoints
Exactly one of these is registered, matching the active provider:
| Provider | Path | Handler |
|---|---|---|
| Stripe | POST /webhooks/stripe | billing/stripe/webhook.ts |
| Polar | POST /webhooks/polar | billing/polar/webhook.ts |
| Lemon Squeezy | POST /webhooks/lemon-squeezy | billing/lemonSqueezy/webhook.ts |
Stripe Events
The Stripe webhook handler processes these events:
| Event | Action |
|---|---|
checkout.session.completed | Upserts the billing customer mapping |
customer.subscription.created | Creates subscription + syncs grants |
customer.subscription.updated | Updates subscription + syncs grants |
customer.subscription.deleted | Revokes grants |
payment_intent.succeeded | Handles lifetime purchases |
charge.refunded | Revokes lifetime grants on full refund |
Event Processing Flow
All provider webhooks follow the same pattern:
- Verify signature — the provider component or handler verifies the webhook signature using the webhook secret.
- Resolve organization — find the organization from webhook metadata, customer mapping, or subscription records.
- Map plan — resolve the provider's product/price/variant ID to a
planKeyusingproviderMapping.ts. - Sync grants — call
syncBillingGrants()with the resolved data. - Update subscription — upsert the
billing_subscriptionsrecord.
Idempotency
The grant system is idempotent:
- Each webhook event has a unique
eventId. - The
billing_event_sourcestable tracks the last processed event per source. - Duplicate or stale events are ignored without modifying grants.
Webhook Secrets
Each provider requires a webhook secret environment variable:
| Provider | Variable |
|---|---|
| Stripe | STRIPE_WEBHOOK_SECRET |
| Polar | POLAR_WEBHOOK_SECRET |
| Lemon Squeezy | LEMON_SQUEEZY_WEBHOOK_SECRET |
Setting Up Webhooks
Development
For local development, use the provider's CLI to forward webhooks to your Convex deployment:
# Stripe
stripe listen --forward-to https://<your-convex-url>/webhooks/stripe
# Polar — use the Polar dashboard to set a webhook URL
# Lemon Squeezy — use the dashboard to set a webhook URLProduction
Set the webhook URL in your provider's dashboard to:
https://<your-convex-deployment-url>/webhooks/<provider>Subscription Email Notifications
The Stripe webhook handler can send subscription confirmation emails. Render a
template with the renderSubscriptionSuccessEmail / renderSubscriptionErrorEmail
helpers in email/templates/subscriptionEmail.tsx and send it through the
sendEmail facade — see
Notification Emails for the snippet. Wire this
into your webhook handler when ready.
Next Reads
- Stripe — Stripe-specific setup.
- Polar — Polar-specific setup.
- Lemon Squeezy — Lemon Squeezy-specific setup.
- Entitlements and Grants — how webhook events become grants.
Customer Portal
The customer portal lets users manage their billing details (update payment method, view invoices, cancel subscription) through the payment provider's hosted UI.
Removing Billing
If your product does not need billing, you can disable it with a single env var or remove the billing system entirely. Billing is opt-in and ships disabled — with BILLING_PROVIDER unset, the system reports all organizations as "unconfigured" and no checkout flows or webhook routes exist.