Docs
Real Stripe and Razorpay integration, not a payment-form mockup — signature-verified webhooks, idempotent event processing, and automated email notifications on both providers.
If a deployment only configures one provider — Stripe and Razorpay are alternatives for different markets, not a required pair — the other provider’s webhook endpoint still returns a clean 200 instead of falling through to signature verification (which would always fail with no webhook secret configured, returning 400). Both providers retry a non-2xx response indefinitely, so without this a webhook to an intentionally-unused provider would retry forever as pure log noise.
Endpoint: POST /api/v1/webhooks/stripe. Signature verified against STRIPE_WEBHOOK_SECRET before any event is processed; anything else falls through to a logged “Unhandled Stripe event type” rather than an error.
| Event | Effect |
|---|---|
| checkout.session.completed | Subscription activated, welcome email sent. |
| invoice.paid | Invoice record created, receipt email sent. |
| invoice.payment_failed | Payment-failed email sent. |
| invoice.payment_action_required | Handles Stripe’s 3D Secure / SCA re-authentication flow. |
| customer.subscription.updated | Status and period-end updated, reflected in the dashboard. |
| customer.subscription.paused / .resumed | Routed through the same handler as updated — both carry a full Subscription object with the new status. |
| customer.subscription.trial_will_end | Trial-ending reminder email. |
| customer.subscription.deleted | Subscription canceled, cancellation date recorded, confirmation email sent. |
Endpoint: POST /api/v1/webhooks/razorpay, verified via X-Razorpay-Signature (HMAC-SHA256 against RAZORPAY_WEBHOOK_SECRET). A payload that passes signature verification but isn’t shaped like a real Razorpay event envelope (missing event/payload fields) is rejected with 400 rather than crashing a handler with a raw type error.
| Event | Effect |
|---|---|
| subscription.activated | Activate the subscription. |
| subscription.charged | Create an invoice record, send a receipt. |
| subscription.cancelled | Cancel the subscription. |
| payment.failed | Handle the failed payment. |
| payment.refunded | Record the refund. |
Both handlers atomically claim an incoming event (provider + event ID) before doing any processing — a genuine duplicate delivery arriving concurrently with the first can’t slip past a check-then-record race and run the business logic twice. A claimed-but-already-processed event returns { received: true, duplicate: true } immediately. A processing failure is marked failed and retried (up to 3 attempts) rather than silently dropped.
# Stripe CLI — forwards webhooks to your local API
stripe listen --forward-to localhost:12002/api/v1/webhooks/stripe
stripe trigger checkout.session.completed
# Razorpay — use ngrok for a public HTTPS webhook URL in dev
ngrok http 12002
# then point the Razorpay dashboard webhook at the ngrok HTTPS URLSTRIPE_SECRET_KEY starts with sk_live_, RAZORPAY_KEY_ID starts with rzp_live_