Webhooks
Understand payment webhook signature verification, idempotency, and subscription/order/entitlement sync.
The payment webhook route is fixed:
app/api/payment/webhook/route.ts
POST /api/payment/webhookIt is not under a locale route. Provider dashboards should never use /zh, /en, or any other language prefix for this URL.
Processing flow
- The Route Handler reads raw
request.text()so providers can verify the original body. getPaymentProvider()selects the provider fromwebsiteConfig.payment.provider.- The provider adapter verifies the signature and maps the event to
PaymentWebhookAction. processPaymentWebhook()writespayment_webhook_eventsfor idempotency.- The processor creates or updates
payment_customers,payment_subscriptions, andpayment_orders. - Successful subscriptions or orders grant or refresh
user_entitlementsand writequota_transactions.
Main actions
| Action | Local effect |
|---|---|
subscription.sync | Sync subscription status, period, plan, price, and grant entitlements for a first subscription |
subscription.payment_succeeded | Handle renewal and refresh or accumulate subscription entitlements |
subscription.cancel | Sync cancellation state |
order.payment_succeeded | Record one-time order and grant its entitlements |
Stripe maps Checkout, Invoice, and Subscription events into these actions. Other providers should map to the same action model so business logic stays provider-agnostic.
Notifications and side effects
The notification module provides notifyOnUserSubscription and notifyOnUserPayment, but the webhook processor calls are commented out by default. Enable them only after configuring Notifications. Notification failures should not break payment processing.
Troubleshooting
Webhook returns Missing webhook signature
Set the provider signing secret, for example STRIPE_WEBHOOK_SECRET.
Checkout succeeds but local subscription does not update
Confirm the provider webhook points to /api/payment/webhook. For local testing, check Stripe CLI forwarding and port.
Event repeats
payment_webhook_events.event_id provides idempotency. Replayed events should be recognized and skipped when already processed.
Entitlements are not granted
Confirm the provider Price ID exactly matches a prices[].priceId in src/modules/billing/config/plan.ts, and that the price has entitlements.
Checklist
- Webhook URL has no locale prefix.
- Signing secret matches the current provider environment.
-
payment_webhook_eventsrecords event ID and status. - Successful subscription creates
payment_subscriptionsanduser_entitlements. - Replaying the same event does not duplicate entitlements.