RAMSIO

Docs

Billing Setup

Real Stripe and Razorpay integration, not a payment-form mockup — signature-verified webhooks, idempotent event processing, and automated email notifications on both providers.

Unconfigured provider, not a broken one

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.

Stripe webhook events

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.

EventEffect
checkout.session.completedSubscription activated, welcome email sent.
invoice.paidInvoice record created, receipt email sent.
invoice.payment_failedPayment-failed email sent.
invoice.payment_action_requiredHandles Stripe’s 3D Secure / SCA re-authentication flow.
customer.subscription.updatedStatus and period-end updated, reflected in the dashboard.
customer.subscription.paused / .resumedRouted through the same handler as updated — both carry a full Subscription object with the new status.
customer.subscription.trial_will_endTrial-ending reminder email.
customer.subscription.deletedSubscription canceled, cancellation date recorded, confirmation email sent.

Razorpay webhook events

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.

EventEffect
subscription.activatedActivate the subscription.
subscription.chargedCreate an invoice record, send a receipt.
subscription.cancelledCancel the subscription.
payment.failedHandle the failed payment.
payment.refundedRecord the refund.

Idempotency

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.

Testing payments locally

# 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 URL

Going live checklist

  • All API keys switched from test to live
  • Stripe webhook signed with the live signing secret
  • Razorpay webhook configured for the production URL
  • STRIPE_SECRET_KEY starts with sk_live_, RAZORPAY_KEY_ID starts with rzp_live_
  • A real checkout tested with a small live amount before launch
  • Webhook delivery verified in each provider’s dashboard event log