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:
Payment types: saas_invoice, fee_payment, admission_fee, canteen_order
Workflow:
- Looks up organization (name, subdomain, Razorpay keys)
- Resolves payable via
resolvePayable()— fetches real amount from DB (Invoice, FeeRecord, etc.) - Validates
return_urlagainst org's domains - Checks for existing active
PaymentOrder→409 PAYMENT_ALREADY_IN_PROGRESS - Creates Razorpay order (platform keys for SaaS, org keys for fees/canteen)
- Creates
PaymentOrder(status:CREATED) - Creates
PaymentAttempt(stage:OTP_PENDING) - Generates 6-digit OTP, hashes with bcrypt (12 rounds)
- Creates
BillingHandoffwith hashed token + hashed OTP - Sends OTP email via
PAYMENT_OTP_SENTtemplate - Returns checkout URL
Response:
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:
Limits:
- Max resends:
MAX_OTP_RESENDS - Cooldown:
OTP_RESEND_COOLDOWN_MSbetween resends
Response:
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:
POST /verify-otp
Verifies the 6-digit OTP and unlocks the Razorpay payment widget.
Request Body:
Security:
- OTP compared via
bcrypt.compare()(constant-time) - Max 3 failed attempts → 15-minute lockout
- Single-use: blocked if
otpVerifiedAtalready set
Response (success):
POST /confirm
Confirms payment after Razorpay checkout completes. Verifies signature, finalizes payment, sends receipt email with PDF attachment.
Request Body:
Workflow:
- Validates handoff: must be active, OTP verified, not consumed
- Verifies
order_idmatches handoff'srazorpay_order_id - Signature verification:
- SaaS invoice →
razorpayService.verifyPlatformSignature() - Fee/canteen →
razorpayService.verifySignature(orgId, ...)
- SaaS invoice →
- Fetches payment from Razorpay API to confirm
- Calls
finalizeCapturedPayment()— creates transaction, updates records - Generates PDF invoice via
generateInvoicePdfBuffer() - Sends confirmation email with PDF attachment from
billing@classgrid.in
Response:
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:
GET /status
Returns whether demo mode is active and if a live session exists.
Response:
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:
- Try platform secret (
RAZORPAY_WEBHOOK_SECRET/RAZORPAY_KEY_SECRET) - Try org's
fees_razorpay_webhook_secret - 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.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
Plan & Module Catalog
Base path: /api/superadmin/billing/catalog
Source: super-admin/billing-catalog.routes.js
Invoice Management
Base path: /api/superadmin/billing/invoices
Source: super-admin/billing-invoice.routes.js
Transaction Management
Base path: /api/superadmin/billing/transactions
Source: super-admin/billing-transactions.routes.js
Revenue Analytics
Base path: /api/superadmin/billing/revenue
Source: super-admin/billing-revenue.routes.js
Failed Payments
Base path: /api/superadmin/billing/failures
Source: super-admin/billing-failures.routes.js
Discounts, Credits & Taxes
Base path: /api/superadmin/billing/discounts-taxes
Source: super-admin/billing-discounts-taxes.routes.js
Eligibility, Pricing & Usage
Base path: /api/superadmin/billing/eligibility-pricing
Source: super-admin/billing-eligibility-pricing.routes.js
Export Jobs
Base path: /api/superadmin/billing/exports
Source: super-admin/billing-exports.routes.js