Razorpay Subscription Flow
Complete documentation for the subscription lifecycle in Zishes.
Webhook Events Processing
1. subscription.activated
Trigger: First successful payment after subscription creation
Processing:
- ✅ Sets
activeRazorpaySubscriptionto subscription ID - ✅ Sets
activeRazorpayPlanto internal plan ID - ✅ Clears
razorpaySubscriptionCancelledAtandrazorpaySubscriptionEndsAt - ❌ Does NOT send notification (notification sent via
invoice.paid) - ❌ Does NOT credit points (points credited via
invoice.paid)
2. subscription.charged
Trigger: Payment is successfully charged (initial + recurring billing cycles)
Processing:
- ❌ Event is logged but no action taken
- ✅ points are credited via
invoice.paidevent instead
3. invoice.paid ⭐ (Main Event for points)
Trigger: Invoice is paid for subscription billing cycle
Processing:
- ✅ Credits points to user's wallet (India wallet)
- ✅ Creates ledger entry with:
- Type:
TOPUP - Amount: points from plan
- Country:
India - Note:
Razorpay subscription rzp:invoice:\{invoiceId\}
- Type:
- ✅ Sends "Plan purchase successful" notification showing points credited
- ✅ Idempotency using invoice ID (prevents duplicate processing)
Notification:
- Title: "Plan purchase successful"
- Body: "You received {X} points from {Plan Name}."
- Type:
PLAN_PURCHASE_SUCCESS
4. subscription.cancelled
Trigger: Subscription is cancelled (via API or by Razorpay)
Processing:
- ✅ Clears ALL subscription fields:
activeRazorpaySubscription→nullactiveRazorpayPlan→nullrazorpaySubscriptionCancelledAt→nullrazorpaySubscriptionEndsAt→null
- ✅ Sends "Subscription cancelled" notification
Notification:
- Title: "Subscription cancelled"
- Body: "We're sad to see you go! Your [Plan Name] subscription has been cancelled. We hope to see you back soon!"
- Type:
SUBSCRIPTION_CANCELLED
5. subscription.completed / subscription.halted / subscription.paused
Trigger: Subscription reaches end of term or is halted/paused
Processing:
- ✅ Clears all subscription-related fields
- ❌ No notification sent
6. subscription.resumed
Trigger: Paused subscription is resumed
Processing:
- ✅ Reactivates subscription (same as
subscription.activated) - ✅ Clears cancellation flags
- ❌ No notification sent (user already knows they resumed)
7. subscription.pending
Trigger: Subscription created but payment not yet processed
Processing:
- ❌ Does NOT set any fields on user
- ❌ No notification sent
- ⏳ Waits for
subscription.activatedorsubscription.charged
Complete User Journey
Scenario 1: Successful Subscription
1. User creates subscription
↓
2. subscription.pending webhook
→ No action taken
↓
3. User completes payment
↓
4. subscription.activated webhook
→ User.activeRazorpaySubscription = "sub_XXX"
→ User.activeRazorpayPlan = "plan_XXX"
→ No notification yet
↓
5. invoice.paid webhook (immediately after activation)
→ Wallet credited with points
→ Ledger entry created with country "India"
→ Notification: "You received X points from Plan Name"
↓
6. subscription.charged webhook (also fires but no action)
→ Event logged only, points already credited by invoice.paid
↓
7. Next billing cycle (e.g., 30 days later)
→ invoice.paid webhook
→ Wallet credited again
→ Notification: "You received X points from Plan Name"
Scenario 2: User Cancels via API
1. User calls /api/v1/payments/razorpay/unsubscribe
→ User.razorpaySubscriptionCancelledAt = NOW
→ User.razorpaySubscriptionEndsAt = END_DATE
→ User.activeRazorpaySubscription = "sub_XXX" (KEPT - user still has access)
→ User.activeRazorpayPlan = "plan_XXX" (KEPT - user still has access)
→ Notification: "Subscription cancelled"
↓
2. User continues to have access until END_DATE
↓
3. subscription.cancelled webhook arrives at END_DATE
→ Clears all fields (IDs + cancellation dates)
→ Notification: "Subscription cancelled" (sent again)
Scenario 3: Razorpay Auto-Cancels (Payment Failure)
1. subscription.cancelled webhook
→ Clears ALL fields
→ Notification: "Subscription cancelled"
→ No cancellation dates recorded (since user didn't request it)
Database Schema
User Model Fields
{
activeRazorpaySubscription: string | null, // Subscription ID from Razorpay
activeRazorpayPlan: string | null, // Internal plan _id
razorpaySubscriptionCancelledAt: Date | null, // Set by API, cleared by webhook
razorpaySubscriptionEndsAt: Date | null // Set by API, cleared by webhook
}
Ledger Entry (created by subscription.charged)
{
user: ObjectId, // User _id
type: "TOPUP", // Always TOPUP for subscriptions
amount: 50, // points from plan
note: "Razorpay subscription rzp:payment:pay_XXX",
country: "India", // Always India for Razorpay
createdAt: Date
}
Notification Types
- SUBSCRIPTION_ACTIVATED // When subscription becomes active
- SUBSCRIPTION_CANCELLED // When subscription is cancelled
- PLAN_PURCHASE_SUCCESS // When points are credited
API Endpoints
Create Subscription
POST /api/v1/payments/razorpay/subscribe
{
"planId": "68d6abdbe69dfbf48f4ee9aa",
"totalCount": 12,
"customerNotify": false
}
Response:
{
"subscription": {
"id": "sub_XXX",
"short_url": "https://rzp.io/i/XXX",
"status": "created"
}
}
Note: Subscription is NOT immediately set on user. Wait for subscription.activated webhook.
Cancel Subscription
POST /api/v1/payments/razorpay/unsubscribe
Response:
{
"ok": true,
"subscription": { "id": "sub_XXX", "status": "cancelled" },
"cancelledAt": "2025-10-06T10:30:00.000Z",
"endsAt": "2025-11-06T10:30:00.000Z"
}
Note: Cancels at end of current billing cycle. Cancellation dates are recorded but will be cleared when webhook arrives.
Get Active Subscription
GET /api/v1/payments/razorpay/subscription
Response:
{
"subscription": { /* Razorpay subscription object */ },
"plan": { /* Internal plan object */ },
"cancelledAt": "2025-10-06T10:30:00.000Z",
"endsAt": "2025-11-06T10:30:00.000Z"
}
Important Notes
-
Country-Specific: Razorpay subscriptions are India-only. All wallet operations use
country: "India". -
Idempotency: Uses payment ID (
pay_XXX) to prevent duplicate point crediting. -
Notes Structure:
userIdis insubscription.entity.notesplanIdis inpayment.entity.notes
-
Notifications: Two separate notifications are sent:
SUBSCRIPTION_ACTIVATEDwhen subscription startsPLAN_PURCHASE_SUCCESSwhen points are credited (each billing cycle)
-
Cancellation: API records cancellation dates temporarily, webhook clears everything when confirmed.
-
Recurring:
subscription.chargedfires for EVERY billing cycle, crediting points each time.