Kulcare Docs

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.com

Authentication

Most endpoints require authentication via Bearer token:

Authorization: Bearer <your_jwt_token>

API Endpoints Summary

EndpointMethodAuthDescription
/billing/subscription_plansGETNoList plans (filter by region/tier/cycle)
/billing/subscription_plans/:idGETNoGet plan details
/billing/subscriptions/currentGETYesGet current subscription
/billing/subscriptions/initiatePOSTYesInitiate checkout (returns gateway data)
/billing/subscriptions/verifyPOSTYesVerify payment and create subscription
/billing/subscriptions/change_planPOSTYesUpgrade/downgrade
/billing/subscriptions/:id/cancelPOSTYesCancel subscription
/billing/subscriptions/:id/invoicesGETYesGet subscription invoices
/billing/payment_methodsGETYesList payment methods
/billing/payment_methodsPOSTYesAdd payment method
/billing/payment_methods/:idDELETEYesRemove payment method
/billing/payment_methods/:id/set_defaultPUTYesSet default
/billing/promo_codes/validatePOSTYesValidate 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:

ParamTypeDescription
regionstringFilter by region: IN, US, AE
tierstringFilter by tier: free, starter, professional, enterprise
billing_cyclestringFilter 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:

  1. Initiate - Creates a gateway subscription, returns checkout data
  2. 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:

FieldTypeRequiredDescription
plan_idUUIDYesPlan to subscribe to
facility_idUUIDNoWhich location the purchase is made FROM. Provenance, not scope (#2165) — omit it and the tenant's oldest facility stands in.
promo_codestringNoPromo 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, trial or past_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):

FieldTypeRequiredDescription
payment_idstringYesGateway payment ID
subscription_idstringYesGateway subscription ID
signaturestringYesPayment signature for verification
plan_idUUIDYesPlan that was subscribed to
facility_idUUIDNoWhich location the purchase was made FROM (#2165). Same fallback as initiate.
promo_codestringNoPromo code applied during initiate
customer_idUUIDNoBilling 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, active or past_due — Billing::Subscription.blocking_a_new_subscription, wider than /initiate, which lets a pending row 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

StatusAccess LevelDescription
trialFull accessFree trial period (plan.trial_days)
activeFull accessPaid subscription
past_dueDegraded (7-day grace)Payment failed
cancelledRead-onlyCancelled by user
expiredRead-onlySubscription ended
pausedRead-onlyTemporarily paused (future)

8. Testing Flows

Full Subscription Creation (Browser)

  1. Log in with a user who has owner role on a facility
  2. Navigate to /account/subscription
  3. Select a plan (Essential or Premium)
  4. Click "Subscribe" then "Confirm Subscribe"
  5. Razorpay checkout modal opens
  6. Use test card: 4111 1111 1111 1111, any future expiry, any CVV
  7. Payment succeeds, subscription created
  8. 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

EventAction
subscription.chargedPayment successful, update to active
subscription.authenticatedPayment method authenticated
subscription.haltedPayment failed, update to past_due
subscription.updatedPlan changed externally
subscription.cancelledCancelled 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 NumberDescription
4111 1111 1111 1111Successful payment
5267 3181 8797 5449Mastercard success
4000 0000 0000 0002Card declined

For UPI test: Use any valid format (e.g., success@razorpay)


11. Common Error Codes

CodeDescription
validation_failedRequest validation failed
record_not_foundResource not found
invalid_promo_codePromo code invalid, expired, or exhausted
payment_verification_failedGateway signature mismatch
region_mismatchPlan region does not match facility
cannot_cancelSubscription cannot be cancelled in current state

On this page