Skip to main content

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 activeRazorpaySubscription to subscription ID
  • ✅ Sets activeRazorpayPlan to internal plan ID
  • ✅ Clears razorpaySubscriptionCancelledAt and razorpaySubscriptionEndsAt
  • ❌ 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.paid event 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\}
  • ✅ 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:
    • activeRazorpaySubscriptionnull
    • activeRazorpayPlannull
    • razorpaySubscriptionCancelledAtnull
    • razorpaySubscriptionEndsAtnull
  • ✅ 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.activated or subscription.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

  1. Country-Specific: Razorpay subscriptions are India-only. All wallet operations use country: "India".

  2. Idempotency: Uses payment ID (pay_XXX) to prevent duplicate point crediting.

  3. Notes Structure:

    • userId is in subscription.entity.notes
    • planId is in payment.entity.notes
  4. Notifications: Two separate notifications are sent:

    • SUBSCRIPTION_ACTIVATED when subscription starts
    • PLAN_PURCHASE_SUCCESS when points are credited (each billing cycle)
  5. Cancellation: API records cancellation dates temporarily, webhook clears everything when confirmed.

  6. Recurring: subscription.charged fires for EVERY billing cycle, crediting points each time.