Billing API

Last updated August 2, 2026

Classgrid's billing system spans 4 route files for the checkout flow + 1 webhook + 8 super-admin route files for billing management.


Part 1: Billing Handoff (Secure Checkout Initiation)

Base path: /api/billing/handoff
Source: billing-handoff.routes.js (254 lines)
Rate Limit: generalLimiter on all endpoints

POST /initiate

Creates a secure checkout session. Resolves the payable from server-side data (amount, recipient, merchant are NOT accepted from client).

Auth: isAuthenticated

Request Body:

JSON
{
  "organization_id": "<ObjectId>",
  "payment_type": "saas_invoice",
  "reference_id": "<invoice ObjectId>",
  "return_url": "https://ganesha.classgrid.in/billing"
}

Payment types: saas_invoice, fee_payment, admission_fee, canteen_order

Workflow:

  1. Looks up organization (name, subdomain, Razorpay keys)
  2. Resolves payable via resolvePayable() — fetches real amount from DB (Invoice, FeeRecord, etc.)
  3. Validates return_url against org's domains
  4. Checks for existing active PaymentOrder409 PAYMENT_ALREADY_IN_PROGRESS
  5. Creates Razorpay order (platform keys for SaaS, org keys for fees/canteen)
  6. Creates PaymentOrder (status: CREATED)
  7. Creates PaymentAttempt (stage: OTP_PENDING)
  8. Generates 6-digit OTP, hashes with bcrypt (12 rounds)
  9. Creates BillingHandoff with hashed token + hashed OTP
  10. Sends OTP email via PAYMENT_OTP_SENT template
  11. Returns checkout URL

Response:

JSON
{
  "success": true,
  "data": {
    "checkout_url": "https://billing.classgrid.in/checkout?token=<base64url>",
    "expiresAt": "2026-08-02T23:55:00Z"
  }
}

Handoff TTL: Configured via HANDOFF_TTL_MS constant.

Rollback: If any step fails after order creation, the system auto-cancels the PaymentOrder, fails the PaymentAttempt, and expires the BillingHandoff.


POST /resend-otp

Resends OTP for an active checkout session.

Request Body:

JSON
{
  "token": "<base64url token from checkout URL>"
}

Limits:

  • Max resends: MAX_OTP_RESENDS
  • Cooldown: OTP_RESEND_COOLDOWN_MS between resends

Response:

JSON
{
  "success": true,
  "message": "OTP resent successfully"
}

Part 2: Billing Checkout (Payment Completion)

Base path: /api/billing/checkout
Source: billing-checkout.routes.js (259 lines)
Rate Limit: generalLimiter on all endpoints

GET /session

Returns checkout session details for the payment page UI.

Query Params: token — the base64url token from the checkout URL

Response:

JSON
{
  "success": true,
  "data": {
    "organizationName": "Ganesha Engineering College",
    "maskedEmail": "r***@college.edu",
    "amountPaise": 150000,
    "currency": "INR",
    "paymentType": "saas_invoice",
    "label": "Classgrid Platform — August 2026",
    "expiresAt": "2026-08-02T23:55:00Z",
    "otpVerified": false
  }
}

POST /verify-otp

Verifies the 6-digit OTP and unlocks the Razorpay payment widget.

Request Body:

JSON
{
  "token": "<base64url>",
  "otp": "482916",
  "payerName": "Rahul Sharma",
  "payerEmail": "rahul@college.edu"
}

Security:

  • OTP compared via bcrypt.compare() (constant-time)
  • Max 3 failed attempts → 15-minute lockout
  • Single-use: blocked if otpVerifiedAt already set

Response (success):

JSON
{
  "success": true,
  "data": {
    "razorpay_order_id": "order_xxx",
    "razorpay_key_id": "rzp_live_xxx",
    "amountPaise": 150000,
    "currency": "INR",
    "email": "rahul@college.edu",
    "return_url": "https://ganesha.classgrid.in/billing"
  }
}

POST /confirm

Confirms payment after Razorpay checkout completes. Verifies signature, finalizes payment, sends receipt email with PDF attachment.

Request Body:

JSON
{
  "token": "<base64url>",
  "razorpay_payment_id": "pay_xxx",
  "razorpay_order_id": "order_xxx",
  "razorpay_signature": "<HMAC SHA256 signature>"
}

Workflow:

  1. Validates handoff: must be active, OTP verified, not consumed
  2. Verifies order_id matches handoff's razorpay_order_id
  3. Signature verification:
    • SaaS invoice → razorpayService.verifyPlatformSignature()
    • Fee/canteen → razorpayService.verifySignature(orgId, ...)
  4. Fetches payment from Razorpay API to confirm
  5. Calls finalizeCapturedPayment() — creates transaction, updates records
  6. Generates PDF invoice via generateInvoicePdfBuffer()
  7. Sends confirmation email with PDF attachment from billing@classgrid.in

Response:

JSON
{
  "success": true,
  "data": {
    "transactionId": "<ObjectId>",
    "providerPaymentId": "pay_xxx",
    "return_url": "https://ganesha.classgrid.in/billing"
  }
}

Part 3: Billing Demo (Razorpay Review)

Base path: /api/billing/demo
Source: billing-demo.routes.js (220 lines)
Guard: BILLING_DEMO_ENABLED env var

POST /session

Creates a 48-hour demo checkout session with OTP 123456 and ₹2 amount.

Response:

JSON
{
  "success": true,
  "data": {
    "checkout_url": "https://billing.classgrid.in/checkout?token=...",
    "demo_otp": "123456",
    "amount": "₹2",
    "expires_at": "2026-08-04T23:25:00Z",
    "test_card": {
      "number": "4111 1111 1111 1111",
      "expiry": "12/27",
      "cvv": "123",
      "otp": "123456"
    },
    "test_upi": "success@razorpay"
  }
}

GET /status

Returns whether demo mode is active and if a live session exists.

Response:

JSON
{
  "enabled": true,
  "has_active_session": true,
  "expires_at": "2026-08-04T23:25:00Z"
}

Part 4: Razorpay Universal Webhook

Base path: /api/webhooks
Source: razorpay-webhook.routes.js (504 lines)

POST /razorpay

Single centralized webhook handling ALL Razorpay events across the platform. Uses express.raw() for raw body signature verification.

Auth: Razorpay x-razorpay-signature header (HMAC SHA256)

Signature verification cascade:

  1. Try platform secret (RAZORPAY_WEBHOOK_SECRET / RAZORPAY_KEY_SECRET)
  2. Try org's fees_razorpay_webhook_secret
  3. Try org's canteen_config.canteen_razorpay_webhook_secret (decrypted)

Idempotency: Creates WebhookEvent with unique providerEventId from x-razorpay-event-id. Duplicate events return 200 { received: true, duplicate: true }.

Handled Events:

payment.captured / payment.authorized

Routes based on notes.type in the payment:

Payment TypeAction
saas_invoice / platformCreates PlatformTransaction, updates SaasInvoice to paid, extends OrgSubscription by 31 days
fee_payment / student_feeCreates FeeTransaction, updates FeeRecord.paid_amount and status
admission_feeDelegates to admission.controller.handlePaymentWebhook()
canteen_orderUpdates CanteenOrder status to NEW, emits Socket.IO canteen_new_order event
marketplace_orderNo-op (logged)
UnknownCreates generic PlatformTransaction for audit trail

payment.failed

Creates a PlatformTransaction with status: "failed", logs error code and description.

order.paid

Confirmation event — no action (payment already handled in payment.captured).

refund.created / refund.processed

Creates refund PlatformTransaction, updates original transaction to status: "refunded".

Always returns 200 to Razorpay to prevent retries.


Part 5: Super Admin Billing APIs

All super-admin billing routes require super_admin role (enforced at router mount level in super-admin.routes.js).

Subscription Management

Base path: /api/superadmin/billing/subscriptions
Source: super-admin/billing-subscription.routes.js

MethodPathDescription
GET/List all org subscriptions
GET/overviewSubscription overview stats
GET/:organizationIdGet specific org's subscription
POST/:organizationId/previewPreview subscription change
POST/:organizationId/assign-planAssign a plan to org
POST/:organizationId/change-planChange org's plan
POST/:organizationId/add-moduleAdd a module to subscription
POST/:organizationId/remove-moduleRemove a module
POST/:organizationId/change-cycleChange billing cycle
POST/:organizationId/pausePause subscription
POST/:organizationId/resumeResume subscription
POST/:organizationId/cancelCancel subscription
GET/:organizationId/historySubscription change history
GET/:organizationId/upcoming-invoicePreview next invoice

Plan & Module Catalog

Base path: /api/superadmin/billing/catalog
Source: super-admin/billing-catalog.routes.js

MethodPathDescription
GET/plansList all plans
POST/plansCreate a new plan
GET/plans/:planIdGet plan details
PATCH/plans/:planId/eligibilityUpdate plan eligibility rules
POST/plans/:planId/versionsCreate new plan version
GET/plans/:planId/versionsList plan versions
POST/plans/:planId/archiveArchive a plan
GET/modulesList all add-on modules
POST/modulesCreate a module
GET/modules/:moduleIdGet module details
PATCH/modules/:moduleId/eligibilityUpdate module eligibility
POST/modules/:moduleId/versionsCreate module version
GET/modules/:moduleId/versionsList module versions
POST/modules/:moduleId/archiveArchive a module

Invoice Management

Base path: /api/superadmin/billing/invoices
Source: super-admin/billing-invoice.routes.js

MethodPathDescription
GET/List all invoices
GET/:invoiceIdGet invoice details
POST/previewPreview invoice before generating
POST/generateGenerate invoice for an org
POST/:invoiceId/issueIssue (finalize) an invoice
POST/:invoiceId/sendSend invoice to org via email
POST/:invoiceId/voidVoid an invoice
POST/:invoiceId/credit-notesCreate a credit note
GET/:invoiceId/pdfDownload invoice PDF
GET/:invoiceId/delivery-historyEmail delivery history

Transaction Management

Base path: /api/superadmin/billing/transactions
Source: super-admin/billing-transactions.routes.js

MethodPathDescription
GET/List all transactions
GET/:transactionIdGet transaction details
POST/:transactionId/recheckRe-verify with Razorpay
POST/:transactionId/refundCreate refund
POST/:transactionId/reconcileManual reconciliation
GET/:transactionId/webhooksView related webhook events
GET/:transactionId/timelineTransaction timeline/audit

Revenue Analytics

Base path: /api/superadmin/billing/revenue
Source: super-admin/billing-revenue.routes.js

MethodPathDescription
GET/Revenue overview dashboard
GET/by-organizationRevenue breakdown by org
GET/by-moduleRevenue breakdown by module
GET/by-invoiceRevenue breakdown by invoice
GET/exportExport revenue data
POST/exportExport revenue data (POST)
POST/reconcileReconcile revenue records

Failed Payments

Base path: /api/superadmin/billing/failures
Source: super-admin/billing-failures.routes.js

MethodPathDescription
GET/List failed payments
GET/overviewFailure stats overview
GET/:failureIdGet failure details
POST/:failureId/generate-payment-linkGenerate retry payment link
POST/:failureId/retry-webhookRetry webhook processing
POST/:failureId/recheck-providerRe-verify with Razorpay
POST/:failureId/notify-organizationSend notification to org
POST/:failureId/diagnostic-exportExport diagnostic data
POST/:failureId/assignAssign to support agent
POST/:failureId/add-noteAdd internal note
POST/:failureId/resolveMark as resolved

Discounts, Credits & Taxes

Base path: /api/superadmin/billing/discounts-taxes
Source: super-admin/billing-discounts-taxes.routes.js

MethodPathDescription
GET/discountsList all discounts
POST/discountsCreate a discount
PATCH/discounts/:discountIdUpdate a discount
POST/discounts/:discountId/archiveArchive a discount
GET/organizations/:orgId/creditsGet org credit account
POST/organizations/:orgId/credits/grantGrant credits to org
POST/organizations/:orgId/credits/reverseReverse credits
GET/tax-rulesList tax rules
POST/tax-rulesCreate tax rule
GET/tax-rules/:taxRuleId/versionsList tax rule versions
POST/tax-rules/:taxRuleId/versionsCreate tax rule version

Eligibility, Pricing & Usage

Base path: /api/superadmin/billing/eligibility-pricing
Source: super-admin/billing-eligibility-pricing.routes.js

MethodPathDescription
GET/eligibility-rulesList eligibility rules
POST/eligibility-rulesCreate eligibility rule
PATCH/eligibility-rules/:ruleIdUpdate eligibility rule
GET/metricsList billing metrics
GET/organizations/:orgId/usageGet org usage data
POST/organizations/:orgId/recalculate-usageRecalculate usage
GET/organizations/:orgId/price-overridesList price overrides
POST/organizations/:orgId/price-overridesCreate price override
PATCH/price-overrides/:overrideIdUpdate price override
DELETE/price-overrides/:overrideIdDelete price override

Export Jobs

Base path: /api/superadmin/billing/exports
Source: super-admin/billing-exports.routes.js

MethodPathDescription
GET/:jobIdGet export job status
GET/:jobId/downloadDownload export file

Key Models

ModelStoragePurpose
BillingHandoffMongoDBCheckout session (token, OTP, amount, Razorpay order)
PaymentOrderMongoDBRazorpay order tracking (status: CREATED → ATTEMPTED → PAID)
PaymentAttemptMongoDBPer-attempt tracking (OTP_PENDING → OTP_VERIFIED → CAPTURED/FAILED)
PlatformTransactionMongoDBPlatform SaaS payment records
FeeTransactionMongoDBStudent fee payment records
SaasInvoiceMongoDBMonthly SaaS invoices for orgs
OrgSubscriptionMongoDBOrg subscription (plan, status, expiresAt)
WebhookEventMongoDBIdempotent webhook event log
Was this helpful?
M↓Markdown supportedMessage is optional