Facility Services API
Facility Services API for managing the service catalog and per-doctor pricing at a facility.
Base URL: /clinical/facilities/:facility_id/facility_services
Per-Doctor Pricing Model (PR #1470, re-homed on
catalog.pricesin #1838): Each facility has a service catalog (facility_services) with adefault_price. Each doctor (practitioner role) can set their own price. Prices are stored incatalog.prices(the single price store since ADR B.3); the per-doctor row carries apractitioner_iddimension holding the doctor'sPractitionerRoleid, and the facility default is thepractitioner_id NULLrow. Thepricingssub-resource and the embeddedpricingsarray are the read/write surface over that store. Billing resolves the doctor's configured price first, falling back todefault_price.Related docs:
- Service Pricing API:
pricingsendpoints (below)- Billing integration:
patient-services-api.md,docs/brainstorms/2026-06-08-billing-packages-problem-and-solution.md- Procedure catalog:
facility-procedures-api.md
Authentication
All requests require authentication via Bearer token:
Authorization: Bearer <token>Data Models
FacilityService (Catalog Entry)
| Field | Type | Description |
|---|---|---|
id | uuid | Unique identifier |
name | string | Service name (unique per facility) |
default_price | decimal | Default/suggested price (10,2). Advisory only — billing uses per-doctor pricing. |
default_duration | integer | Default duration in minutes (optional) |
is_active | boolean | Whether the service is active |
service_type | enum | Type: consultation, telemedicine, opd, ipd, nursing, follow_up, other |
created_at | datetime | Creation timestamp |
updated_at | datetime | Last update timestamp |
formatted_default_price | string | Default price formatted as string |
pricings | array | Per-doctor pricing records. Always embedded (every view) since #1838 — include_pricings no longer gates it (it only selects the richer detailed view). |
FacilityServicePricing (Per-Doctor Pricing)
| Field | Type | Description |
|---|---|---|
id | uuid | Unique identifier |
facility_service_id | uuid | Reference to catalog service |
practitioner_role_id | uuid | Reference to doctor's practitioner role (the catalog.prices.practitioner_id dimension) |
price | decimal | Doctor's price for this service |
is_active | boolean | Whether this pricing is active (listings return active rows only) |
practitioner_role | object | Light embedded role info: { id, display_name } |
duration/custom_namewere dropped with the oldfacility_service_pricingstable (B.3) and are no longer returned.
Service Type Auto-Categorization
When creating a service, if service_type is not provided, it will be auto-categorized based on the service name:
- Names matching
*teleconsultation*→telemedicine - Names matching
*consultation*→consultation - Names matching
*follow*up*→follow_up - All other names →
other
Endpoints
1. List Facility Services
GET /clinical/facilities/:facility_id/facility_services
Retrieve all services for a facility with filtering and sorting.
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
search_text | string | - | Search by service name (case-insensitive) |
service_type | string | - | Filter by type: consultation, telemedicine, opd, ipd, nursing, follow_up, other |
include_inactive | boolean | false | Include inactive services |
include_pricings | boolean | false | Include per-doctor pricing records for each service |
sort_by_type | string | created_at | Sort field: name, default_price, created_at |
sort_by_order | string | desc | Sort order: asc, desc |
page | integer | 1 | Page number |
per_page | integer | 25 | Items per page |
Request Example
curl -X GET "https://api.kulcare.com/clinical/facilities/{facility_id}/facility_services?search_text=consult&sort_by_type=name&sort_by_order=asc&include_pricings=true" \
-H "Authorization: Bearer TOKEN"Response Example
{
"data": [
{
"id": "018f1234-5678-abcd-ef01-23456789abcd",
"name": "General Consultation",
"default_price": 500.00,
"formatted_default_price": "500.00",
"default_duration": 30,
"service_type": "consultation",
"is_active": true,
"pricings": [
{
"id": "018f1234-5678-abcd-ef01-23456789abce",
"facility_service_id": "018f1234-5678-abcd-ef01-23456789abcd",
"practitioner_role_id": "018f1234-5678-abcd-ef01-practitioner1",
"price": 600.00,
"duration": 30,
"custom_name": null,
"is_active": true,
"practitioner_role": {
"id": "018f1234-5678-abcd-ef01-practitioner1",
"display_name": "Dr. Raj Sharma",
"practitioner": {
"id": "018f1234-5678-abcd-ef01-doc1",
"full_name": "Dr. Raj Sharma"
}
}
}
]
}
],
"meta": {
"request_id": "req_abc123",
"pagination": {
"page": 1,
"per_page": 25,
"total": 1
}
}
}2. Get Facility Service
GET /clinical/facilities/:facility_id/facility_services/:id
Retrieve a single facility service by ID.
Request Example
curl -X GET "https://api.kulcare.com/clinical/facilities/{facility_id}/facility_services/{id}" \
-H "Authorization: Bearer TOKEN"Response Example
{
"data": {
"id": "018f1234-5678-abcd-ef01-23456789abcd",
"name": "General Consultation",
"default_price": 500.00,
"formatted_default_price": "500.00",
"default_duration": 30,
"is_active": true,
"service_type": "consultation",
"created_at": "2025-02-10T10:00:00Z",
"updated_at": "2025-02-10T10:00:00Z",
"facility": {
"id": "018f9876-5432-dcba-fedc-0987654321ab",
"name": "City Health Clinic"
}
},
"meta": {
"request_id": "req_abc123"
}
}3. Create Facility Service
POST /clinical/facilities/:facility_id/facility_services
Create a new facility service (catalog entry). Optionally creates a per-doctor pricing record if practitioner_role_id is provided (for doctor custom entries).
Request Body
{
"facility_service": {
"name": "General Consultation",
"default_price": 500.00,
"default_duration": 30,
"is_active": true,
"service_type": "consultation"
}
}Fields
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Service name (unique per facility) |
default_price | decimal | No | Default/suggested price (>= 0). Optional — doctors set their own price. |
default_duration | integer | No | Default duration in minutes (> 0) |
is_active | boolean | No | Active status (default: true) |
service_type | string | No | Type: consultation, telemedicine, opd, ipd, nursing, follow_up, other |
practitioner_role_id | uuid | No | If provided, auto-creates a pricing record for this doctor (used for custom entries) |
Request Example
curl -X POST "https://api.kulcare.com/clinical/facilities/{facility_id}/facility_services" \
-H "Authorization: Bearer TOKEN" \
-H "Content-Type: application/json" \
-d '{
"facility_service": {
"name": "General Consultation",
"default_price": 500.00,
"default_duration": 30,
"service_type": "consultation"
}
}'Response Example (201 Created)
{
"data": {
"id": "018f1234-5678-abcd-ef01-23456789abcd",
"name": "General Consultation",
"default_price": 500.00,
"formatted_default_price": "500.00",
"default_duration": 30,
"is_active": true,
"service_type": "consultation",
"created_at": "2025-02-10T10:00:00Z",
"updated_at": "2025-02-10T10:00:00Z",
"facility": {
"id": "018f9876-5432-dcba-fedc-0987654321ab",
"name": "City Health Clinic"
}
},
"meta": {
"request_id": "req_abc123"
}
}Error Response (422 Unprocessable Entity)
{
"data": null,
"error": {
"code": "validation_error",
"message": "Validation failed",
"details": [
{
"field": "name",
"message": "General Consultation already exists"
}
]
},
"meta": {
"request_id": "req_abc123"
}
}4. Update Facility Service
PATCH /clinical/facilities/:facility_id/facility_services/:id
Update an existing facility service (catalog entry).
Request Body
All fields are optional.
{
"facility_service": {
"name": "Specialist Consultation",
"default_price": 750.00,
"is_active": false
}
}Request Example
curl -X PATCH "https://api.kulcare.com/clinical/facilities/{facility_id}/facility_services/{id}" \
-H "Authorization: Bearer TOKEN" \
-H "Content-Type: application/json" \
-d '{
"facility_service": {
"default_price": 750.00
}
}'Response Example
{
"data": {
"id": "018f1234-5678-abcd-ef01-23456789abcd",
"name": "General Consultation",
"default_price": 750.00,
"formatted_default_price": "750.00",
"default_duration": 30,
"is_active": true,
"service_type": "consultation",
"created_at": "2025-02-10T10:00:00Z",
"updated_at": "2025-02-10T11:30:00Z",
"facility": {
"id": "018f9876-5432-dcba-fedc-0987654321ab",
"name": "City Health Clinic"
}
},
"meta": {
"request_id": "req_abc123"
}
}5. Delete Facility Service
DELETE /clinical/facilities/:facility_id/facility_services/:id
Delete a facility service (catalog entry).
Blocking rule: Cannot delete a service that has active per-doctor pricing records. All pricings must be removed first. This prevents accidentally deleting a catalog entry that doctors are actively using.
Request Example
curl -X DELETE "https://api.kulcare.com/clinical/facilities/{facility_id}/facility_services/{id}" \
-H "Authorization: Bearer TOKEN"Response Example (204 No Content)
Empty response body with status code 204 No Content.
Error Response (422 — Has Active Pricings)
{
"data": null,
"error": {
"code": "validation_error",
"message": "Cannot delete service with active pricings. Remove all doctor pricings first.",
"details": [
{
"field": "base",
"message": "Cannot delete service with active pricings. Remove all doctor pricings first."
}
]
},
"meta": {
"request_id": "req_abc123"
}
}Per-Doctor Pricing Endpoints
6. List Service Pricings
GET /clinical/facilities/:facility_id/facility_services/:facility_service_id/pricings
Retrieve all per-doctor pricing records for a specific service.
Returns the active per-doctor pricing records for the service (the facility-default row is not included — it is managed on the parent service via default_price).
Response Example
{
"data": [
{
"id": "018f1234-5678-abcd-ef01-23456789abce",
"facility_service_id": "018f1234-5678-abcd-ef01-23456789abcd",
"practitioner_role_id": "018f1234-5678-abcd-ef01-practitioner1",
"price": 600.00,
"is_active": true,
"practitioner_role": {
"id": "018f1234-5678-abcd-ef01-practitioner1",
"display_name": "Dr. Raj Sharma"
}
}
]
}7. Create Service Pricing
POST /clinical/facilities/:facility_id/facility_services/:facility_service_id/pricings
Create a per-doctor pricing record for a service.
Request Body
{
"facility_service_pricing": {
"practitioner_role_id": "018f1234-5678-abcd-ef01-practitioner1",
"price": 600.00
}
}Fields
| Field | Type | Required | Description |
|---|---|---|---|
practitioner_role_id | uuid | Yes | Doctor's practitioner role at this facility (must belong to the facility, else 422) |
price | decimal | Yes | Doctor's price (must be positive) |
createis an upsert on(service, role): a second POST for the same doctor updates the price rather than creating a duplicate.
8. Update Service Pricing
PATCH /clinical/facilities/:facility_id/facility_services/:facility_service_id/pricings/:id
Update a per-doctor pricing record's price.
9. Delete Service Pricing (Hard Delete)
DELETE /clinical/facilities/:facility_id/facility_services/:facility_service_id/pricings/:id
Permanently remove a doctor's pricing for this service. This is a hard delete — no soft delete / deleted_at. The doctor can always re-create the pricing later.
Price Resolution for Billing
When billing a service for a patient, price is resolved as follows:
- Frontend sends
facility_service_id+ a per-linepractitioner_id - Backend maps it to
PractitionerRole.find_by(practitioner_id:, facility_id:) - Backend resolves the price via
Catalog::PriceResolver(per-doctor row incatalog.pricesfor that role) - If a doctor price is found → use it (the doctor's configured price)
- If none → fall back to
facility_service.default_price - If neither → reject with error
Authoritative server-side enforcement of this resolution (overriding the client-sent unit_price) lands with the resolver wiring in #1848; the billing panel already resolves and previews per #1838.
See docs/brainstorms/2026-06-08-billing-packages-problem-and-solution.md for full billing flow.
Error Codes
| Code | Description |
|---|---|
not_found | Resource not found |
validation_error | Request validation failed |
unauthorized | Authentication required or insufficient permissions |
TypeScript Types
// Service type enum (7 values, maps to ABDM billing codes)
type ServiceType = 'consultation' | 'telemedicine' | 'opd' | 'ipd' | 'nursing' | 'follow_up' | 'other';
// Facility service (catalog entry)
interface FacilityService {
id: string;
name: string;
default_price?: number;
formatted_default_price?: string;
default_duration?: number;
is_active: boolean;
service_type: ServiceType;
created_at: string;
updated_at: string;
pricings?: FacilityServicePricing[];
facility?: {
id: string;
name: string;
};
}
// Per-doctor service pricing
interface FacilityServicePricing {
id: string;
facility_service_id: string;
practitioner_role_id: string;
price: number;
duration?: number;
custom_name?: string | null;
is_active: boolean;
created_at: string;
updated_at: string;
practitioner_role?: {
id: string;
display_name?: string;
full_name?: string;
practitioner?: {
id: string;
full_name?: string;
};
};
}
// List request params
interface ListFacilityServicesParams {
search_text?: string;
service_type?: ServiceType;
include_inactive?: boolean;
include_pricings?: boolean;
sort_by_type?: 'name' | 'default_price' | 'created_at';
sort_by_order?: 'asc' | 'desc';
page?: number;
per_page?: number;
}
// Create request body
interface CreateFacilityServiceRequest {
facility_service: {
name: string;
default_price?: number;
default_duration?: number;
is_active?: boolean;
service_type?: ServiceType;
practitioner_role_id?: string;
};
}
// Update request body
interface UpdateFacilityServiceRequest {
facility_service: {
name?: string;
default_price?: number;
default_duration?: number;
is_active?: boolean;
service_type?: ServiceType;
};
}
// API response wrapper
interface ApiResponse<T> {
data: T | T[] | null;
error?: {
code: string;
message: string;
details: Array<{ field: string; message: string }>;
};
meta: {
request_id: string;
pagination?: {
page: number;
per_page: number;
total: number;
};
};
}