ShipNext
Payment Integration

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/webhook

It is not under a locale route. Provider dashboards should never use /zh, /en, or any other language prefix for this URL.

Processing flow

  1. The Route Handler reads raw request.text() so providers can verify the original body.
  2. getPaymentProvider() selects the provider from websiteConfig.payment.provider.
  3. The provider adapter verifies the signature and maps the event to PaymentWebhookAction.
  4. processPaymentWebhook() writes payment_webhook_events for idempotency.
  5. The processor creates or updates payment_customers, payment_subscriptions, and payment_orders.
  6. Successful subscriptions or orders grant or refresh user_entitlements and write quota_transactions.

Main actions

ActionLocal effect
subscription.syncSync subscription status, period, plan, price, and grant entitlements for a first subscription
subscription.payment_succeededHandle renewal and refresh or accumulate subscription entitlements
subscription.cancelSync cancellation state
order.payment_succeededRecord 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_events records event ID and status.
  • Successful subscription creates payment_subscriptions and user_entitlements.
  • Replaying the same event does not duplicate entitlements.

On this page