0%
Vibe44 Logo

Webhook Setup

Configure payment provider webhooks for real-time events. Each provider needs a URL, specific events, and a signing secret.

Webhook URLs

Replace <YOUR_DOMAIN> with your production domain. For local dev, use ngrok or Stripe CLI.

ProviderURL
Stripehttps://<YOUR_DOMAIN>/api/webhooks/stripe
Polarhttps://<YOUR_DOMAIN>/api/webhooks/polar
Lemon Squeezyhttps://<YOUR_DOMAIN>/api/webhooks/lemon-squeezy
Dodo Paymentshttps://<YOUR_DOMAIN>/api/webhooks/dodo
Creemhttps://<YOUR_DOMAIN>/api/webhooks/creem

Billing Portal Return Sync

When a user opens the customer portal from /dashboard/billing, the application now sets the provider return URL to:

https://<YOUR_DOMAIN>/dashboard/billing?portal_return=1

On return, /dashboard/billing performs an authenticated provider reconciliation by calling PaymentProvider.getSubscription() for the user's current profiles.subscription_id, then routes the normalized result through syncSubscriptionToDb(). This closes the common race where the customer returns before webhook delivery finishes.

Webhooks remain the source of truth. The portal-return reconciliation is a fast, user-scoped read repair. Successful webhook or reconciliation syncs invalidate the billing, dashboard, and user cache tags.

Provider dashboard change: make sure your portal/customer-dashboard settings allow <YOUR_DOMAIN>/dashboard/billing as a return URL or allowed redirect domain.


1. Stripe

Events:

  • checkout.session.completed
  • customer.subscription.created
  • customer.subscription.updated
  • customer.subscription.paused
  • customer.subscription.resumed
  • customer.subscription.deleted
  • invoice.paid
  • invoice.payment_failed

After setup: Copy Webhook Signing Secret (whsec_...) → STRIPE_WEBHOOK_SECRET

Local: stripe listen --forward-to localhost:3000/api/webhooks/stripe


2. Polar

Events:

  • checkout.created
  • checkout.updated
  • checkout.expired
  • order.created
  • order.paid
  • subscription.created
  • subscription.active
  • subscription.updated
  • subscription.canceled
  • subscription.past_due
  • subscription.revoked

After setup: Copy Webhook Secret → POLAR_WEBHOOK_SECRET


3. Lemon Squeezy

Events:

  • order_created
  • subscription_created
  • subscription_updated
  • subscription_cancelled
  • subscription_resumed
  • subscription_expired
  • subscription_paused
  • subscription_unpaused
  • subscription_payment_success
  • subscription_payment_failed
  • subscription_payment_recovered

After setup: Copy Webhook Secret → LEMONSQUEEZY_WEBHOOK_SECRET


4. Dodo Payments

Events:

  • payment.succeeded
  • payment.failed
  • subscription.active
  • subscription.renewed
  • subscription.plan_changed
  • subscription.updated
  • subscription.cancelled
  • subscription.failed
  • subscription.on_hold
  • subscription.expired

After setup: Copy Webhook Secret → DODO_WEBHOOK_SECRET

DODO_WEBHOOK_SECRET is the canonical Dodo webhook variable.


5. Creem

Events:

  • checkout.completed
  • subscription.active
  • subscription.trialing
  • subscription.paid
  • subscription.canceled
  • subscription.scheduled_cancel
  • subscription.expired
  • subscription.paused
  • subscription.update
  • subscription.past_due

After setup: Copy Webhook Secret → CREEM_WEBHOOK_SECRET

Signature Verification

ProviderSignature Header(s)Algorithm
Stripestripe-signatureHMAC-SHA256
Polarwebhook-id, webhook-timestamp, webhook-signatureHMAC-SHA256
Lemon Squeezyx-signatureHMAC-SHA256
Dodowebhook-id, webhook-timestamp, webhook-signatureHMAC-SHA256
Creemcreem-signatureHMAC-SHA256

[!TIP] Always use crypto.timingSafeEqual() when comparing signatures to prevent timing attacks.

Webhook Idempotency

Every provider handler records delivery state in webhook_logs using the (provider, event_id) partial unique index where event_id IS NOT NULL.

Existing row stateBehavior
successSkip; event is already processed
processing under 5 minutes oldSkip; another request owns the work
stale processingReuse row and increment attempts
failedReuse row and increment attempts

This keeps provider retries safe without losing retryability after transient failures.

Provider resource IDs are also unique at the billing-state layer:

  • subscriptions(provider, provider_subscription_id) prevents duplicate active subscription rows and lets syncSubscriptionToDb() upsert.
  • purchases(provider, provider_payment_id) prevents duplicate one-time purchase rows and lets savePurchase() return the existing purchase as idempotent success.

Creem events must include a durable provider event ID (event.id). The handler rejects payloads without that ID instead of generating a timestamp-based fallback, because generated IDs would turn every retry into a new event.

Payment Readiness

Internal health uses lib/payments/readiness.ts to validate the selected provider's API key/access token, webhook secret, and active paid-tier price/product IDs. A webhook secret alone is not enough for launch readiness: checkout still fails if the selected provider's tier product identifiers are missing.

Local Testing

  1. Stripe: Use the Stripe CLI: stripe listen --forward-to localhost:3000/api/webhooks/stripe
  2. Others: Use a tunnel like ngrok: ngrok http 3000. Register the HTTPS URL (e.g., https://xyz.ngrok.io/api/webhooks/[provider]) in the provider's dashboard.

Provider Docs References

Universal Checklist

  1. URL - Must end with correct provider name: stripe, polar, lemon-squeezy, dodo, or creem
  2. Events - Include the listed subscription update/cancel events. They are the real-time source for /dashboard/billing state.
  3. Secret - Signing secret must be in .env.local with the correct var name
  4. Access - Server must be internet-accessible (ngrok for local)
  5. Headers - Polar and Dodo require webhook-id, webhook-timestamp, webhook-signature for HMAC. Handler passes full headers automatically
  6. Return URL - Allow https://<YOUR_DOMAIN>/dashboard/billing in the provider portal settings if the provider restricts portal redirects.
  7. Idempotency - Confirm duplicate success events skip, active processing rows skip, failed/stale processing events retry, and duplicate provider subscription/payment IDs do not create duplicate billing rows.
  8. Creem Event IDs - Confirm Creem sandbox webhooks include durable event.id values before enabling production webhooks.

Copyright © 2026 Vibe44. Built with ❤️ by Scale with Alap.