← knowledge.oriz.in

Razorpay end-to-end setup — TEST keys + 4 plans + 4 promos + webhook + E2E test + LIVE

runbook runbookbillingrazorpaysubscriptionswebhookenvsecrets

Razorpay end-to-end setup

This runbook walks Chirag through every remaining Razorpay step after initial signup. The Sole-Proprietor (PAN-only) account is already created, KYC is in progress / done, and the 4 subscription plans have already been created — their IDs are already in c:/D/oriz/.env. What's left is API keys, a webhook, 4 promo codes, integration in @chirag127/astro-billing, and an end-to-end test with the test card before flipping to LIVE.

Plain English, checkbox style. Do these in order.


0. Quick bootstrap (recommended)

Run node scripts/razorpay-bootstrap.mjs once. It does all of sections 2-5 automatically: creates the 4 plans, the 4 offers, the webhook (with the 9-event set), and 4 subscription links — all idempotent (safe to re-run; skips what already exists, updates what changed).

cd c:/D/oriz
node scripts/razorpay-bootstrap.mjs           # live, idempotent
node scripts/razorpay-bootstrap.mjs --dry     # preview, no writes
node scripts/razorpay-bootstrap.mjs --verbose # log every API call

After it succeeds, captured IDs / URLs are written back into .env (RAZORPAY_PLAN_*, RAZORPAY_OFFER_*, RAZORPAY_LINK_*). The script also tries to re-encrypt .env.enc via sops; if sops isn't on PATH it prints the one-liner to run manually.

Known limitation — Offers API: the public POST /v1/offers endpoint requires the Magic Checkout / Promotions add-on enabled by Razorpay support. If it returns 404, the script prints a clear fallback (create the 4 offers manually in Dashboard → Promotions → Offers; re-run the script to pick them up — it matches by notes.oriz_offer_id or by name === CODE). Bootstrap continues through the remaining steps either way.

After running the script, you can skip to section 7 (E2E test). Sections 2-5 below are kept for reference / manual fallback.


1. Generate TEST mode API keys

Why TEST first: a wrong webhook URL or signature-verification bug in LIVE means real card auths fail. TEST mode has zero risk and uses the same code path.


2. Verify the 4 plans

The plans are already created. Verify each is present and correct before wiring buttons.

Plan Interval Amount (INR) Amount (paise) Plan ID
Pro Monthly Monthly ₹99 9900 plan_T4amiZh5BGgR5g
Pro Yearly Yearly ₹799 79900 plan_T4anE3HWceQDua
Max Monthly Monthly ₹299 29900 plan_T4aoFpRlVnSh4s
Max Yearly Yearly ₹2,499 249900 plan_T4and1y3RYyO64

3. Generate Test Webhook secret

Why these 9 events: covers the full subscription lifecycle — activation, every successful recurring charge, cancel paths, halts (auto-debit failure), and one-off payment.failed for first-time payment failures that don't yet have a subscription entity. We don't subscribe to invoice-level events because Razorpay's subscription events already carry the invoice context.


4. Create 4 promo codes

Code Type Discount Plans Cap Expiry Use case
FOUNDER50 Percent 50% off first month All 4 plans 100 redemptions total None (run until cap hit) Launch promo for early supporters
LAUNCH30 Percent 30% off Pro Yearly + Max Yearly only Unlimited 2026-07-31 23:59 IST Push yearly upgrades in launch month
BLOG20 Percent 20% off All 4 plans Unlimited None Generic blog / social media code
STUDENT50 Percent 50% off Pro Monthly + Pro Yearly only Unlimited None Verified via GitHub Student Pack at checkout

For each offer:

Optional: capture all 4 offer IDs in .env if the integration needs to reference them programmatically:

RAZORPAY_OFFER_FOUNDER50=offer_XXXXXXXXXXXX
RAZORPAY_OFFER_LAUNCH30=offer_XXXXXXXXXXXX
RAZORPAY_OFFER_BLOG20=offer_XXXXXXXXXXXX
RAZORPAY_OFFER_STUDENT50=offer_XXXXXXXXXXXX

(If astro-billing lets the user type the code at checkout and Razorpay validates it server-side, the offer IDs aren't strictly needed — but having them in env makes promo analytics easier.)


5. Implement in @chirag127/astro-billing

Library code lives at: c:/D/oriz/repos/oriz/own/lib/npm/astro-billing-npm-pkg/src/

This runbook doesn't write the code — that's a separate task. The expected file layout is:

File Purpose
src/lib/razorpay-client.ts Thin wrapper over the Razorpay Node SDK; reads RAZORPAY_KEY_ID + RAZORPAY_KEY_SECRET from env; exposes createSubscription({ planId, customerId, offerId? })
src/components/Pricing.astro Renders the 3-tier table (Free / Pro / Max) with monthly/yearly toggle and 4 Checkout buttons
src/components/CheckoutButton.astro Single button bound to a plan ID; on click → calls /api/billing-create-subscription → opens Razorpay Checkout JS modal
src/pages/api/billing-create-subscription.ts CF Pages Function: POST { planId, userId, offerId? } → calls Razorpay → returns { subscription_id }
src/pages/api/billing-webhook/razorpay.ts CF Pages Function: receives Razorpay POST, verifies HMAC, updates Firestore (see section 6)

Button flow (end-user perspective):

  1. User on /pricing clicks "Subscribe Pro Monthly".
  2. Frontend POSTs to /api/billing-create-subscription with { planId: 'plan_T4amiZh5BGgR5g', userId: <firebase uid> }.
  3. The function calls Razorpay's subscriptions.create() API and returns the resulting subscription_id.
  4. Frontend opens Razorpay Checkout JS modal with that subscription_id and the user's prefilled email/name.
  5. User enters card → Razorpay handles 3DS OTP.
  6. On success, Razorpay closes the modal and triggers the webhook.
  7. Webhook handler verifies signature, looks up the user, writes users/{uid}/subscriptions/razorpay in Firestore:
    { "tier": "pro", "interval": "monthly", "status": "active",
      "subscription_id": "sub_…", "plan_id": "plan_…",
      "current_period_end": 1750000000 }
    
  8. The frontend's Firestore listener on that doc fires → React/Astro re-renders → ads disappear, Pro features unlock.

6. Webhook handler — sketch

c:/D/oriz/repos/oriz/own/lib/npm/astro-billing-npm-pkg/src/pages/api/billing-webhook/razorpay.ts

import type { APIRoute } from 'astro';
import { createHmac } from 'node:crypto';
// import { getFirestore } from 'firebase-admin/firestore';

export const POST: APIRoute = async ({ request }) => {
  // 1. Read RAW body (signature is computed over the raw bytes)
  const body = await request.text();
  const signature = request.headers.get('x-razorpay-signature') ?? '';

  // 2. Verify HMAC SHA256
  const expected = createHmac('sha256', process.env.RAZORPAY_WEBHOOK_SECRET!)
    .update(body)
    .digest('hex');

  if (signature !== expected) {
    return new Response('Invalid signature', { status: 401 });
  }

  // 3. Parse and route
  const event = JSON.parse(body);
  const sub = event.payload?.subscription?.entity;
  const payment = event.payload?.payment?.entity;

  // 4. Map event → Firestore update
  // event.event ∈ { subscription.activated | subscription.charged |
  //                 subscription.cancelled | … | payment.failed }
  // sub.id            → subscriptionId
  // sub.customer_id   → look up our userId via users-by-razorpay-customer
  // sub.plan_id       → which tier (lookup PLAN_ID_TO_TIER map)
  // sub.current_end   → unix seconds when current period ends
  //
  // Write: users/{uid}/subscriptions/razorpay
  //   { tier, interval, status, subscription_id, plan_id,
  //     current_period_end, updated_at: serverTimestamp() }

  return new Response('OK', { status: 200 });
};

Things this sketch deliberately leaves out (future task):


7. Test E2E with the Razorpay test card

Test cards (all on TEST mode):

Card Number CVV Expiry 3DS OTP
Domestic success 4111 1111 1111 1111 any 3 digits any future date 123456
Domestic failure 5104 0600 0000 0008 any any future n/a (declines at auth)
International success 5267 3181 8797 5449 any any future 123456

Full list: razorpay.com/docs/payments/payments/test-card-details.

Local E2E flow:

Debug guide if any step fails:

Symptom Likely cause Fix
Modal never opens Frontend JS error in browser console Check /api/billing-create-subscription returned 200 with a subscription_id
401 on webhook Signature mismatch Ensure handler uses raw request body, not JSON.stringify(parsed). Check RAZORPAY_WEBHOOK_SECRET matches what's saved in dashboard
Webhook never fires ngrok URL not saved in dashboard, or webhook disabled Dashboard → Webhooks → Active toggle
Webhook fires, returns 500 Firebase Admin not initialized in CF Pages Function Service-account JSON env var present? CF Pages secret bound correctly?
Subscription active in Razorpay but Firestore doc empty Customer-ID → UID mapping missing Pre-create the razorpay_customers/{customer_id} doc during createSubscription
Firestore updates but UI doesn't Listener on subscription doc not wired Add onSnapshot(users/{uid}/subscriptions/razorpay) in the header component

8. Go LIVE

Once TEST mode has run E2E successfully for at least 1 week with multiple test transactions:


9. Common pitfalls


10. Quick reference — TEST plan IDs

Plan INR Plan ID (TEST) Env var
Pro Monthly ₹99 plan_T4amiZh5BGgR5g RAZORPAY_PLAN_PRO_MONTHLY
Pro Yearly ₹799 plan_T4anE3HWceQDua RAZORPAY_PLAN_PRO_YEARLY
Max Monthly ₹299 plan_T4aoFpRlVnSh4s RAZORPAY_PLAN_MAX_MONTHLY
Max Yearly ₹2,499 plan_T4and1y3RYyO64 RAZORPAY_PLAN_MAX_YEARLY

LIVE plan IDs will replace these after step 8.


Cross-refs