Leave Localhost logoLeave LocalhostDocs
Billing

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:

ProviderPathHandler
StripePOST /webhooks/stripebilling/stripe/webhook.ts
PolarPOST /webhooks/polarbilling/polar/webhook.ts
Lemon SqueezyPOST /webhooks/lemon-squeezybilling/lemonSqueezy/webhook.ts

Stripe Events

The Stripe webhook handler processes these events:

EventAction
checkout.session.completedUpserts the billing customer mapping
customer.subscription.createdCreates subscription + syncs grants
customer.subscription.updatedUpdates subscription + syncs grants
customer.subscription.deletedRevokes grants
payment_intent.succeededHandles lifetime purchases
charge.refundedRevokes lifetime grants on full refund

Event Processing Flow

All provider webhooks follow the same pattern:

  1. Verify signature — the provider component or handler verifies the webhook signature using the webhook secret.
  2. Resolve organization — find the organization from webhook metadata, customer mapping, or subscription records.
  3. Map plan — resolve the provider's product/price/variant ID to a planKey using providerMapping.ts.
  4. Sync grants — call syncBillingGrants() with the resolved data.
  5. Update subscription — upsert the billing_subscriptions record.

Idempotency

The grant system is idempotent:

  • Each webhook event has a unique eventId.
  • The billing_event_sources table 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:

ProviderVariable
StripeSTRIPE_WEBHOOK_SECRET
PolarPOLAR_WEBHOOK_SECRET
Lemon SqueezyLEMON_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 URL

Production

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

On this page