Subscription System API Testing Guide
Date: 2026-04-07 (updated) Purpose: Test the billing subscription APIs including region-specific plans and Razorpay checkout
Base URL
Development: http://localhost:3000
Staging: https://api-stg.kulcare.comAuthentication
Most endpoints require authentication via Bearer token:
Authorization: Bearer <your_jwt_token>API Endpoints Summary
| Endpoint | Method | Auth | Description |
|---|---|---|---|
/billing/subscription_plans | GET | No | List plans (filter by region/tier/cycle) |
/billing/subscription_plans/:id | GET | No | Get plan details |
/billing/subscriptions/current | GET | Yes | Get current subscription |
/billing/subscriptions/initiate | POST | Yes | Initiate checkout (returns gateway data) |
/billing/subscriptions/verify | POST | Yes | Verify payment and create subscription |
/billing/subscriptions/change_plan | POST | Yes | Upgrade/downgrade |
/billing/subscriptions/:id/cancel | POST | Yes | Cancel subscription |
/billing/subscriptions/:id/invoices | GET | Yes | Get subscription invoices |
/billing/payment_methods | GET | Yes | List payment methods |
/billing/payment_methods | POST | Yes | Add payment method |
/billing/payment_methods/:id | DELETE | Yes | Remove payment method |
/billing/payment_methods/:id/set_default | PUT | Yes | Set default |
/billing/promo_codes/validate | POST | Yes | Validate promo code |
1. Subscription Plans (Public, Cached 30 min)
GET /billing/subscription_plans
List active subscription plans filtered by region.
Auth: Not required
Query Parameters:
| Param | Type | Description |
|---|---|---|
region | string | Filter by region: IN, US, AE |
tier | string | Filter by tier: free, starter, professional, enterprise |
billing_cycle | string | Filter by cycle: monthly, quarterly, semi_annual, annual |
Example Request (India plans):
curl -X GET "http://localhost:3000/billing/subscription_plans?region=IN"Response:
{
"data": [
{
"id": "plan-uuid-1",
"name": "Essential",
"tier": "starter",
"billing_cycle": "monthly",
"region": "IN",
"currency": "INR",
"price": "575.84",
"price_inr": "575.84",
"price_usd": "7.0",
"price_aed": "25.0",
"features": {
"patients_per_day": 10,
"queue_appointments": true,
"billing": true,
"video_consultations": true,
"digital_prescriptions": true,
"mobile_apps": true,
"clinic_website": false
},
"limits": {
"monthly_appointments": 300,
"locations": 1,
"doctors": 3,
"staff": 2
},
"trial_days": 14,
"is_active": true
}
]
}Plans are ordered by price ascending (Essential before Premium).
GET /billing/subscription_plans/:id
Get detailed plan information including gateway plan IDs.
curl -X GET "http://localhost:3000/billing/subscription_plans/plan-uuid-1"2. Subscription Checkout Flow (Two-Phase)
The subscription creation uses a two-phase flow:
- Initiate - Creates a gateway subscription, returns checkout data
- Verify - Verifies payment signature, creates local subscription record
POST /billing/subscriptions/initiate
Initiate checkout. Creates a Razorpay customer and subscription, returns data for the frontend checkout modal.
Auth: Required
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
plan_id | UUID | Yes | Plan to subscribe to |
facility_id | UUID | No | Which location the purchase is made FROM. Provenance, not scope (#2165) — omit it and the tenant's oldest facility stands in. |
promo_code | string | No | Promo code for discount |
Example Request:
curl -X POST "http://localhost:3000/billing/subscriptions/initiate" \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"plan_id": "plan-uuid-1",
"facility_id": "facility-uuid"
}'Success Response (200):
{
"data": {
"gateway": "razorpay",
"gateway_subscription_id": "sub_XXXXXXXXXX",
"gateway_key": "rzp_test_XXXXXXXXXX",
"amount": 575.84,
"currency": "INR",
"plan_id": "plan-uuid-1",
"plan_name": "Essential",
"billing_cycle": "monthly",
"trial_days": 14,
"customer_id": "billing-customer-uuid",
"promo_code": null
}
}Error Responses:
- 422: "This plan is not available for your facility's region"
- 422: "This tenant already has a subscription" (status is
active,trialorpast_due) — the block is TENANT-wide (#2165), not per facility - 422: "This plan is not available for online checkout" (missing gateway plan ID)
POST /billing/subscriptions/verify
Verify payment after Razorpay checkout completes. Creates the local subscription record.
Auth: Required
Request Body (gateway-agnostic field names):
| Field | Type | Required | Description |
|---|---|---|---|
payment_id | string | Yes | Gateway payment ID |
subscription_id | string | Yes | Gateway subscription ID |
signature | string | Yes | Payment signature for verification |
plan_id | UUID | Yes | Plan that was subscribed to |
facility_id | UUID | No | Which location the purchase was made FROM (#2165). Same fallback as initiate. |
promo_code | string | No | Promo code applied during initiate |
customer_id | UUID | No | Billing customer ID from initiate response |
Example Request:
curl -X POST "http://localhost:3000/billing/subscriptions/verify" \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"payment_id": "pay_XXXXXXXXXX",
"subscription_id": "sub_XXXXXXXXXX",
"signature": "hmac_sha256_signature_here",
"plan_id": "plan-uuid-1",
"facility_id": "facility-uuid",
"customer_id": "billing-customer-uuid"
}'Success Response (201):
{
"data": {
"id": "sub-uuid-new",
"status": "trial",
"amount": "575.84",
"currency": "INR",
"billing_cycle": "monthly",
"start_date": "2026-04-07",
"end_date": "2026-05-07",
"trial_end_date": "2026-04-21",
"next_billing_date": "2026-04-21",
"gateway_subscription_id": "sub_XXXXXXXXXX",
"gateway": "razorpay",
"plan": { ... }
}
}Error Responses:
- 422: "Payment verification failed -- signature mismatch"
- 422: "Plan is not available for this facility's region"
- 422: "This tenant already has a subscription" (status is
pending,trial,activeorpast_due—Billing::Subscription.blocking_a_new_subscription, wider than /initiate, which lets apendingrow be superseded) — the block is TENANT-wide (#2165), not per facility
3. Current Subscription
GET /billing/subscriptions/current
Get the tenant's current active subscription.
Auth: Required
Query Parameters: none. A subscription covers the TENANT (#2165), which
comes from the bearer token. A facility_id sent here is ignored.
curl -X GET "http://localhost:3000/billing/subscriptions/current" \
-H "Authorization: Bearer <token>"Response when subscription exists:
{
"data": {
"id": "sub-uuid-1",
"plan_id": "plan-uuid-1",
"status": "active",
"billing_cycle": "monthly",
"currency": "INR",
"amount": "575.84",
"start_date": "2026-04-07",
"end_date": "2026-05-07",
"trial_end_date": null,
"next_billing_date": "2026-05-07",
"gateway_subscription_id": "sub_XXXXXXXXXX",
"plan": {
"id": "plan-uuid-1",
"name": "Essential",
"tier": "starter",
"region": "IN",
"currency": "INR",
"price": "575.84"
}
}
}Response when no subscription: { "data": null, "meta": {} }
4. Plan Changes and Cancellation
POST /billing/subscriptions/change_plan
Upgrade or downgrade. Only within same region.
curl -X POST "http://localhost:3000/billing/subscriptions/change_plan" \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"plan_id": "new-plan-uuid"
}'POST /billing/subscriptions/:id/cancel
curl -X POST "http://localhost:3000/billing/subscriptions/sub-uuid/cancel" \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"at_cycle_end": true, "reason": "Switching to a different solution"}'5. Payment Methods
GET /billing/payment_methods
curl -X GET "http://localhost:3000/billing/payment_methods?facility_id=facility-uuid" \
-H "Authorization: Bearer <token>"POST /billing/payment_methods
curl -X POST "http://localhost:3000/billing/payment_methods" \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"facility_id": "facility-uuid",
"method_type": "card",
"gateway_token": "tok_razorpay_xxx",
"last_four": "4242",
"card_brand": "Visa"
}'DELETE /billing/payment_methods/:id
curl -X DELETE "http://localhost:3000/billing/payment_methods/pm-uuid" \
-H "Authorization: Bearer <token>"PUT /billing/payment_methods/:id/set_default
curl -X PUT "http://localhost:3000/billing/payment_methods/pm-uuid/set_default" \
-H "Authorization: Bearer <token>"6. Promo Codes
POST /billing/promo_codes/validate
curl -X POST "http://localhost:3000/billing/promo_codes/validate" \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"code": "WELCOME20", "plan_id": "plan-uuid-1"}'Response:
{
"data": {
"valid": true,
"code": "WELCOME20",
"discount_type": "percentage",
"discount_amount": null,
"discount_percentage": 20.0
}
}7. Subscription Status Values
| Status | Access Level | Description |
|---|---|---|
trial | Full access | Free trial period (plan.trial_days) |
active | Full access | Paid subscription |
past_due | Degraded (7-day grace) | Payment failed |
cancelled | Read-only | Cancelled by user |
expired | Read-only | Subscription ended |
paused | Read-only | Temporarily paused (future) |
8. Testing Flows
Full Subscription Creation (Browser)
- Log in with a user who has
ownerrole on a facility - Navigate to
/account/subscription - Select a plan (Essential or Premium)
- Click "Subscribe" then "Confirm Subscribe"
- Razorpay checkout modal opens
- Use test card:
4111 1111 1111 1111, any future expiry, any CVV - Payment succeeds, subscription created
- Page shows "Current Plan" badge
Full Subscription Creation (API)
# 1. List plans for India
curl -X GET "http://localhost:3000/billing/subscription_plans?region=IN"
# 2. Check no existing subscription
curl -X GET "http://localhost:3000/billing/subscriptions/current" \
-H "Authorization: Bearer TOKEN"
# 3. Initiate checkout
curl -X POST "http://localhost:3000/billing/subscriptions/initiate" \
-H "Authorization: Bearer TOKEN" \
-H "Content-Type: application/json" \
-d '{"plan_id": "PLAN_ID", "facility_id": "FACILITY_ID"}'
# Save gateway_subscription_id from response
# 4. (In browser) Open Razorpay checkout with the subscription_id
# Or skip to step 5 if testing verify directly with known payment data
# 5. Verify payment (after Razorpay callback)
curl -X POST "http://localhost:3000/billing/subscriptions/verify" \
-H "Authorization: Bearer TOKEN" \
-H "Content-Type: application/json" \
-d '{
"payment_id": "pay_XXXXX",
"subscription_id": "sub_XXXXX",
"signature": "HMAC_SIGNATURE",
"plan_id": "PLAN_ID",
"facility_id": "FACILITY_ID"
}'Region Filtering Test
# India plans only
curl "http://localhost:3000/billing/subscription_plans?region=IN"
# Should return 8 plans (Essential + Premium x 4 billing cycles)
# US plans only
curl "http://localhost:3000/billing/subscription_plans?region=US"
# Should return 8 plans with USD pricing
# All plans (no region filter)
curl "http://localhost:3000/billing/subscription_plans"
# Should return 24 active plans (8 per region)Error Case Tests
# Region mismatch (IN facility subscribing to US plan)
curl -X POST "http://localhost:3000/billing/subscriptions/initiate" \
-H "Authorization: Bearer TOKEN" \
-d '{"plan_id": "US_PLAN_ID", "facility_id": "IN_FACILITY_ID"}'
# Expected: 422 "This plan is not available for your facility's region"
# Duplicate subscription
curl -X POST "http://localhost:3000/billing/subscriptions/initiate" \
-H "Authorization: Bearer TOKEN" \
-d '{"plan_id": "PLAN_ID", "facility_id": "ANY_FACILITY_OF_A_SUBSCRIBED_TENANT"}'
# Expected: 422 "This tenant already has an active subscription"
# Any facility of that tenant trips it — the subscription covers the tenant.
# Cross-region plan change
# (subscribe to IN plan, then try changing to US plan)
# Expected: 422 "Cannot change to a plan from a different region"Seed Plans for Testing
cd backend
bundle exec rake billing:seed_go_forward_plans
# Seeds Essential and Premium (IN, monthly), their price rows and their
# Cashfree plan IDs. Safe to re-run. On a development database, db:seed
# does the same.9. Webhook Events
| Event | Action |
|---|---|
subscription.charged | Payment successful, update to active |
subscription.authenticated | Payment method authenticated |
subscription.halted | Payment failed, update to past_due |
subscription.updated | Plan changed externally |
subscription.cancelled | Cancelled from gateway |
Webhook URL: POST /billing/webhooks (legacy) or POST /billing/webhooks/:gateway (new)
Webhook signature verification is required in staging/production. Skipped in development with a warning log.
10. Razorpay Test Cards
| Card Number | Description |
|---|---|
| 4111 1111 1111 1111 | Successful payment |
| 5267 3181 8797 5449 | Mastercard success |
| 4000 0000 0000 0002 | Card declined |
For UPI test: Use any valid format (e.g., success@razorpay)
11. Common Error Codes
| Code | Description |
|---|---|
validation_failed | Request validation failed |
record_not_found | Resource not found |
invalid_promo_code | Promo code invalid, expired, or exhausted |
payment_verification_failed | Gateway signature mismatch |
region_mismatch | Plan region does not match facility |
cannot_cancel | Subscription cannot be cancelled in current state |