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_TOKENBase URLs
- Billing:
/billing - Subscriptions:
/subscriptions - Invoices:
/invoices - Payments:
/payments
Endpoints Overview
Billing Plans
| Method | Endpoint | Description |
|---|---|---|
| GET | /billing_plans | List available billing plans |
| GET | /billing_plans/:id | Get plan details |
Subscriptions
| Method | Endpoint | Description |
|---|---|---|
| GET | /subscriptions | List clinic subscriptions |
| POST | /subscriptions | Create new subscription |
| GET | /subscriptions/:id | Get subscription details |
| PUT | /subscriptions/:id | Update subscription |
| POST | /subscriptions/:id/cancel | Cancel subscription |
| POST | /subscriptions/:id/reactivate | Reactivate subscription |
Invoices
| Method | Endpoint | Description |
|---|---|---|
| GET | /invoices | List invoices |
| POST | /invoices | Create invoice |
| GET | /invoices/:id | Get invoice details |
| PUT | /invoices/:id | Update invoice |
| POST | /invoices/:id/send | Send invoice to patient |
| POST | /invoices/:id/pay | Process payment |
Payments
| Method | Endpoint | Description |
|---|---|---|
| GET | /payments | List payments |
| POST | /payments | Process payment |
| GET | /payments/:id | Get payment details |
| POST | /payments/:id/refund | Refund payment |
Razorpay Integration
| Method | Endpoint | Description |
|---|---|---|
| POST | /razorpay/orders | Create Razorpay order |
| POST | /razorpay/webhooks | Handle Razorpay webhooks |
| POST | /razorpay/verify | Verify payment signature |
Billing Plans
GET /billing_plans
List all available billing plans for clinics.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
country | String | Filter by country (IN/US) |
plan_type | String | Filter 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
| Parameter | Type | Description |
|---|---|---|
status | String | Filter by status (active/cancelled/expired) |
page | Integer | Page 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
| Parameter | Type | Description |
|---|---|---|
status | String | Filter by status (draft/sent/paid/overdue) |
patient_id | Integer | Filter by patient |
start_date | String | Date range start |
end_date | String | Date range end |
page | Integer | Page 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
| Parameter | Type | Description |
|---|---|---|
status | String | Filter by status (success/failed/pending) |
payment_method | String | Filter by method (razorpay/cash/upi) |
start_date | String | Date range start |
end_date | String | Date 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.capturedpayment.failedsubscription.chargedsubscription.cancelledrefund.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. The same revenue is still readable line
by line through the view_bills endpoints (GET /billing/patient_services,
GET /billing/invoices), which practitioners hold by default, so a colleague's
billed revenue remains derivable by summing line items. Narrowing that grain
would change the front desk's own screens and is tracked as 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
| Parameter | Type | Description |
|---|---|---|
start_time | String | Period start, ISO 8601 (required) |
end_time | String | Period end, ISO 8601 (required) |
monthly | Boolean | Accepted 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
| Code | Description |
|---|---|
| 400 | Bad Request - Invalid parameters |
| 401 | Unauthorized - Invalid or missing token |
| 402 | Payment Required - Subscription expired |
| 403 | Forbidden - No permission to access |
| 404 | Not Found - Resource doesn't exist |
| 409 | Conflict - Duplicate invoice/payment |
| 422 | Unprocessable Entity - Validation errors |
| 429 | Too 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.createdsubscription.cancelledsubscription.renewedpayment.successfulpayment.failedinvoice.createdinvoice.paidinvoice.overdue
Webhook Configuration
{
"webhook_url": "https://your-app.com/webhooks/billing",
"events": ["payment.successful", "subscription.renewed"],
"secret": "your_webhook_secret"
}Rate Limits
| Endpoint | Rate Limit |
|---|---|
| Payment processing | 10 requests per minute |
| Invoice creation | 60 requests per hour |
| General billing APIs | 100 requests per hour |
Related Endpoints
- Clinic APIs - Clinic subscription management
- Patient APIs - Patient billing information
- Appointment APIs - Appointment-based billing