Kulcare Docs
DeveloperBilling

Billing APIs

This document covers all billing and payment-related endpoints for managing subscriptions, invoices, transactions, and payment processing.

Authentication

All billing endpoints require authentication. Include the JWT token in the Authorization header:

Authorization: Bearer YOUR_AUTH_TOKEN

Base URLs

  • Billing: /billing
  • Subscriptions: /subscriptions
  • Invoices: /invoices
  • Payments: /payments

Endpoints Overview

Billing Plans

MethodEndpointDescription
GET/billing_plansList available billing plans
GET/billing_plans/:idGet plan details

Subscriptions

MethodEndpointDescription
GET/subscriptionsList clinic subscriptions
POST/subscriptionsCreate new subscription
GET/subscriptions/:idGet subscription details
PUT/subscriptions/:idUpdate subscription
POST/subscriptions/:id/cancelCancel subscription
POST/subscriptions/:id/reactivateReactivate subscription

Invoices

MethodEndpointDescription
GET/invoicesList invoices
POST/invoicesCreate invoice
GET/invoices/:idGet invoice details
PUT/invoices/:idUpdate invoice
POST/invoices/:id/sendSend invoice to patient
POST/invoices/:id/payProcess payment

Payments

MethodEndpointDescription
GET/paymentsList payments
POST/paymentsProcess payment
GET/payments/:idGet payment details
POST/payments/:id/refundRefund payment

Razorpay Integration

MethodEndpointDescription
POST/razorpay/ordersCreate Razorpay order
POST/razorpay/webhooksHandle Razorpay webhooks
POST/razorpay/verifyVerify payment signature

Billing Plans

GET /billing_plans

List all available billing plans for clinics.

Query Parameters

ParameterTypeDescription
countryStringFilter by country (IN/US)
plan_typeStringFilter by plan type

Example Request

curl -X GET "http://localhost:3000/billing_plans?country=IN" \
  -H "Authorization: Bearer YOUR_AUTH_TOKEN"

Example Response

{
  "success": true,
  "data": [
    {
      "id": 1,
      "name": "Basic Plan",
      "description": "Perfect for small clinics",
      "price": 999,
      "currency": "INR",
      "billing_cycle": "monthly",
      "country": "IN",
      "features": {
        "max_doctors": 2,
        "max_patients": 500,
        "appointments_per_month": 1000,
        "sms_quota": 100,
        "video_consultation": true,
        "reports": "basic",
        "support": "email"
      },
      "razorpay_plan_id": "plan_basic_monthly",
      "active": true
    },
    {
      "id": 2,
      "name": "Professional Plan",
      "description": "For growing practices",
      "price": 1999,
      "currency": "INR",
      "billing_cycle": "monthly",
      "country": "IN",
      "features": {
        "max_doctors": 10,
        "max_patients": 2000,
        "appointments_per_month": 5000,
        "sms_quota": 500,
        "video_consultation": true,
        "reports": "advanced",
        "support": "priority"
      },
      "razorpay_plan_id": "plan_pro_monthly",
      "active": true
    }
  ]
}

GET /billing_plans/:id

Get detailed information about a specific billing plan.

Example Request

curl -X GET http://localhost:3000/billing_plans/1 \
  -H "Authorization: Bearer YOUR_AUTH_TOKEN"

Subscriptions

GET /subscriptions

List all subscriptions for the authenticated clinic.

Query Parameters

ParameterTypeDescription
statusStringFilter by status (active/cancelled/expired)
pageIntegerPage number

Example Request

curl -X GET http://localhost:3000/subscriptions \
  -H "Authorization: Bearer YOUR_AUTH_TOKEN"

Example Response

{
  "success": true,
  "data": [
    {
      "id": 123,
      "clinic_id": 101,
      "billing_plan_id": 1,
      "status": "active",
      "start_date": "2025-01-01",
      "end_date": "2025-02-01",
      "auto_renew": true,
      "razorpay_subscription_id": "sub_123456789",
      "current_period_start": "2025-01-01",
      "current_period_end": "2025-02-01",
      "plan": {
        "id": 1,
        "name": "Basic Plan",
        "price": 999,
        "currency": "INR",
        "billing_cycle": "monthly"
      },
      "usage": {
        "appointments_used": 45,
        "appointments_limit": 1000,
        "sms_used": 12,
        "sms_limit": 100,
        "doctors_count": 1,
        "doctors_limit": 2
      },
      "next_billing_date": "2025-02-01",
      "created_at": "2025-01-01T00:00:00Z"
    }
  ]
}

POST /subscriptions

Create a new subscription for a clinic.

Request Body

{
  "subscription": {
    "billing_plan_id": 1,
    "auto_renew": true,
    "payment_method": "razorpay"
  }
}

Example Request

curl -X POST http://localhost:3000/subscriptions \
  -H "Authorization: Bearer YOUR_AUTH_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "subscription": {
      "billing_plan_id": 1,
      "auto_renew": true
    }
  }'

Success Response

{
  "success": true,
  "data": {
    "id": 123,
    "status": "pending_payment",
    "razorpay_order": {
      "id": "order_123456789",
      "amount": 99900,
      "currency": "INR",
      "key": "rzp_test_123456789"
    },
    "payment_url": "https://checkout.razorpay.com/v1/checkout.js"
  },
  "message": "Subscription created. Complete payment to activate."
}

POST /subscriptions/:id/cancel

Cancel an active subscription.

Request Body

{
  "cancellation_reason": "Switching to different software",
  "cancel_at_period_end": true
}

Example Request

curl -X POST http://localhost:3000/subscriptions/123/cancel \
  -H "Authorization: Bearer YOUR_AUTH_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "cancellation_reason": "No longer needed",
    "cancel_at_period_end": false
  }'

Invoices

GET /invoices

List invoices with filtering options.

Query Parameters

ParameterTypeDescription
statusStringFilter by status (draft/sent/paid/overdue)
patient_idIntegerFilter by patient
start_dateStringDate range start
end_dateStringDate range end
pageIntegerPage number

Example Request

curl -X GET "http://localhost:3000/invoices?status=unpaid" \
  -H "Authorization: Bearer YOUR_AUTH_TOKEN"

Example Response

{
  "success": true,
  "data": [
    {
      "id": 456,
      "invoice_number": "INV-2025-001",
      "patient_id": 789,
      "appointment_id": 123,
      "status": "unpaid",
      "issue_date": "2025-01-15",
      "due_date": "2025-01-22",
      "subtotal": 500.00,
      "tax_amount": 90.00,
      "discount": 0.00,
      "total_amount": 590.00,
      "currency": "INR",
      "patient": {
        "id": 789,
        "name": "John Doe",
        "phone_number": "9999999999",
        "email": "john.doe@email.com"
      },
      "items": [
        {
          "id": 1,
          "description": "Consultation Fee",
          "quantity": 1,
          "unit_price": 500.00,
          "total": 500.00
        }
      ],
      "payments": [],
      "created_at": "2025-01-15T10:00:00Z"
    }
  ],
  "meta": {
    "page": 1,
    "per_page": 20,
    "total": 5
  }
}

POST /invoices

Create a new invoice.

Request Body

{
  "invoice": {
    "patient_id": 789,
    "appointment_id": 123,
    "due_date": "2025-01-22",
    "items": [
      {
        "description": "Consultation Fee",
        "quantity": 1,
        "unit_price": 500.00
      },
      {
        "description": "Medicine",
        "quantity": 2,
        "unit_price": 50.00
      }
    ],
    "discount": 10.00,
    "tax_rate": 18.0,
    "notes": "Payment due within 7 days"
  }
}

Example Request

curl -X POST http://localhost:3000/invoices \
  -H "Authorization: Bearer YOUR_AUTH_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "invoice": {
      "patient_id": 789,
      "items": [
        {
          "description": "Consultation",
          "quantity": 1,
          "unit_price": 500.00
        }
      ]
    }
  }'

POST /invoices/:id/send

Send invoice to patient via SMS/email.

Request Body

{
  "send_via": ["sms", "email"],
  "custom_message": "Your consultation invoice is ready"
}

Example Request

curl -X POST http://localhost:3000/invoices/456/send \
  -H "Authorization: Bearer YOUR_AUTH_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "send_via": ["sms"]
  }'

POST /invoices/:id/pay

Process payment for an invoice.

Request Body

{
  "payment": {
    "amount": 590.00,
    "payment_method": "razorpay",
    "razorpay_payment_id": "pay_123456789",
    "razorpay_signature": "signature_hash"
  }
}

Payments

GET /payments

List all payments with filtering.

Query Parameters

ParameterTypeDescription
statusStringFilter by status (success/failed/pending)
payment_methodStringFilter by method (razorpay/cash/upi)
start_dateStringDate range start
end_dateStringDate range end

Example Request

curl -X GET http://localhost:3000/payments \
  -H "Authorization: Bearer YOUR_AUTH_TOKEN"

Example Response

{
  "success": true,
  "data": [
    {
      "id": 789,
      "invoice_id": 456,
      "amount": 590.00,
      "currency": "INR",
      "payment_method": "razorpay",
      "status": "success",
      "razorpay_payment_id": "pay_123456789",
      "razorpay_order_id": "order_987654321",
      "transaction_fee": 15.00,
      "net_amount": 575.00,
      "processed_at": "2025-01-15T14:30:00Z",
      "patient": {
        "id": 789,
        "name": "John Doe"
      },
      "invoice": {
        "id": 456,
        "invoice_number": "INV-2025-001"
      }
    }
  ]
}

POST /payments

Process a new payment.

Request Body

{
  "payment": {
    "invoice_id": 456,
    "amount": 590.00,
    "payment_method": "razorpay",
    "razorpay_payment_id": "pay_123456789",
    "razorpay_order_id": "order_987654321",
    "razorpay_signature": "signature_hash"
  }
}

POST /payments/:id/refund

Process a refund for a payment.

Request Body

{
  "refund": {
    "amount": 590.00,
    "reason": "Duplicate payment",
    "notes": "Customer paid twice"
  }
}

Example Request

curl -X POST http://localhost:3000/payments/789/refund \
  -H "Authorization: Bearer YOUR_AUTH_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "refund": {
      "amount": 590.00,
      "reason": "Service not provided"
    }
  }'

Razorpay Integration

POST /razorpay/orders

Create a Razorpay order for payment processing.

Request Body

{
  "order": {
    "amount": 59000,
    "currency": "INR",
    "receipt": "INV-2025-001",
    "notes": {
      "invoice_id": "456",
      "patient_id": "789"
    }
  }
}

Example Request

curl -X POST http://localhost:3000/razorpay/orders \
  -H "Authorization: Bearer YOUR_AUTH_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "order": {
      "amount": 59000,
      "currency": "INR",
      "receipt": "INV-2025-001"
    }
  }'

Success Response

{
  "success": true,
  "data": {
    "id": "order_123456789",
    "entity": "order",
    "amount": 59000,
    "amount_paid": 0,
    "amount_due": 59000,
    "currency": "INR",
    "receipt": "INV-2025-001",
    "status": "created",
    "created_at": 1642147200,
    "key": "rzp_test_123456789"
  }
}

POST /razorpay/verify

Verify Razorpay payment signature.

Request Body

{
  "razorpay_order_id": "order_123456789",
  "razorpay_payment_id": "pay_987654321",
  "razorpay_signature": "signature_hash_here"
}

Example Request

curl -X POST http://localhost:3000/razorpay/verify \
  -H "Authorization: Bearer YOUR_AUTH_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "razorpay_order_id": "order_123456789",
    "razorpay_payment_id": "pay_987654321",
    "razorpay_signature": "signature_hash"
  }'

POST /razorpay/webhooks

Handle Razorpay webhook notifications.

Webhook Events

  • payment.captured
  • payment.failed
  • subscription.charged
  • subscription.cancelled
  • refund.created

Example Webhook Payload

{
  "entity": "event",
  "account_id": "acc_123456789",
  "event": "payment.captured",
  "contains": ["payment"],
  "payload": {
    "payment": {
      "entity": {
        "id": "pay_123456789",
        "amount": 59000,
        "currency": "INR",
        "status": "captured",
        "order_id": "order_987654321",
        "created_at": 1642147200
      }
    }
  },
  "created_at": 1642147260
}

Usage Tracking

GET /billing/usage

Get current billing period usage for the clinic.

Example Request

curl -X GET http://localhost:3000/billing/usage \
  -H "Authorization: Bearer YOUR_AUTH_TOKEN"

Example Response

{
  "success": true,
  "data": {
    "subscription_id": 123,
    "billing_period": {
      "start": "2025-01-01",
      "end": "2025-02-01"
    },
    "usage": {
      "appointments": {
        "used": 45,
        "limit": 1000,
        "percentage": 4.5
      },
      "sms": {
        "used": 12,
        "limit": 100,
        "percentage": 12.0
      },
      "doctors": {
        "count": 1,
        "limit": 2
      },
      "patients": {
        "count": 234,
        "limit": 500
      }
    },
    "overages": [],
    "next_reset": "2025-02-01"
  }
}

Billing Summary

GET /billing/facilities/:facility_id/summary

Get the facility's billing summary and analytics: billed and collected revenue, GST, daywise revenue, the previous-month comparison, and by_practitioner (every practitioner's billed revenue).

Authorization

This is practice finance, not front-desk data. The caller needs view_practice_finance or manage_practice_finance in the facility's tenant (Billing::BillingSummaryPolicy#show?; seeded for owner and admin only). Patient-bill keys (view_bills, add_bills, edit_bills) and view_own_commissions do not reach it. A member without the key gets 403; a facility outside the caller's tenant is 404 (#2276). A Kulcare platform admin needs a membership in the tenant that holds one of the keys, as with the premium reports — a membership alone is no longer enough. (Every Billing endpoint already required one: verify_tenant_access! raises without it.)

This gate closes the aggregate only, and the line-by-line grain is now narrowed for practitioners but not closed.

Invoices are encounter-scoped for a practitioner (#2273). InvoicePolicy::Scope resolves to:

CallerSees
neither view_bills nor edit_billsnothing
holds edit_bills, or is not a practitionerthe whole tenant
a practitionerinvoices from encounters they took part in, in any participant role

An invoice with no encounter is invisible to a practitioner — a pharmacy-only or package-gating invoice belongs to nobody. That is a stated decision, not an artefact of the join.

Every invoice read path resolves through that scope, so an invoice the caller may not read is 404, not 403 — the pair was otherwise an existence oracle over invoice ids. Three routes carry it: GET /billing/invoices, GET /billing/patient_facilities/:id/invoices (the list the CMS Bills tab and the queue read, which renders line items), and the per-invoice show / items routes on both controllers. The patient list applies the scope before filtering and pagination, so a practitioner's page 1 is their own first page.

GET /billing/patient_services is NOT narrowed, so a colleague's billed revenue remains partly derivable by summing service lines. Narrowing that grain would change the front desk's own screens and is still a separate product decision.

In the CMS, the Revenue page (/cms/reports/revenue, route-gated on view_reports) shows its Summary tab only to holders of one of those two keys. Anyone else gets the page without it, opening on Services, and a direct /cms/reports/revenue/summary link redirects to the page's default route (PracticeAnalyticsPage.tsx).

Query Parameters

ParameterTypeDescription
start_timeStringPeriod start, ISO 8601 (required)
end_timeStringPeriod end, ISO 8601 (required)
monthlyBooleanAccepted but currently ignored by SummaryService; series are daywise

Example Response

{
  "success": true,
  "data": {
    "period": {
      "start": "2025-01-01",
      "end": "2025-01-31"
    },
    "revenue": {
      "total": 15600.00,
      "consultations": 12000.00,
      "procedures": 3600.00,
      "currency": "INR"
    },
    "invoices": {
      "total": 26,
      "paid": 22,
      "unpaid": 3,
      "overdue": 1
    },
    "payments": {
      "cash": 4500.00,
      "online": 11100.00,
      "total": 15600.00
    },
    "subscription": {
      "plan": "Basic Plan",
      "status": "active",
      "next_billing": "2025-02-01",
      "amount": 999.00
    }
  }
}

Error Codes

CodeDescription
400Bad Request - Invalid parameters
401Unauthorized - Invalid or missing token
402Payment Required - Subscription expired
403Forbidden - No permission to access
404Not Found - Resource doesn't exist
409Conflict - Duplicate invoice/payment
422Unprocessable Entity - Validation errors
429Too Many Requests - Rate limit exceeded

Common Error Messages

{
  "success": false,
  "error": "Subscription expired",
  "details": "Your subscription expired on 2025-01-15. Please renew to continue using the service."
}
{
  "success": false,
  "error": "Payment verification failed",
  "details": "Razorpay signature verification failed"
}
{
  "success": false,
  "error": "Usage limit exceeded",
  "details": "You have exceeded your monthly appointment limit of 1000"
}

Webhooks

Configure webhook endpoints to receive billing event notifications:

Webhook Events

  • subscription.created
  • subscription.cancelled
  • subscription.renewed
  • payment.successful
  • payment.failed
  • invoice.created
  • invoice.paid
  • invoice.overdue

Webhook Configuration

{
  "webhook_url": "https://your-app.com/webhooks/billing",
  "events": ["payment.successful", "subscription.renewed"],
  "secret": "your_webhook_secret"
}

Rate Limits

EndpointRate Limit
Payment processing10 requests per minute
Invoice creation60 requests per hour
General billing APIs100 requests per hour

On this page