Facility Procedures API
Facility Procedures API for managing the procedure catalog and per-doctor pricing at a facility.
Base URL: /clinical/facilities/:facility_id/facility_procedures
Per-Doctor Pricing Model (PR #1470, re-homed on
catalog.pricesin #1838): Each facility has a procedure catalog (facility_procedures) 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. Billing resolves the doctor's configured price first, falling back todefault_price.Related docs:
- Procedure Pricing API:
pricingsendpoints (below)- Billing integration:
docs/brainstorms/2026-06-08-billing-packages-problem-and-solution.md- Service catalog:
facility-services-api.md
Authentication
All requests require authentication via Bearer token:
Authorization: Bearer <token>Data Models
FacilityProcedure (Catalog Entry)
| Field | Type | Description |
|---|---|---|
id | uuid | Unique identifier |
name | string | Procedure 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) |
category | string | Procedure category (optional, max 100 chars) |
description | text | Procedure description (optional) |
is_active | boolean | Whether the procedure is active |
insurance_covered | boolean | Whether insurance covers this procedure |
international_options_included | boolean | International patient options available |
finance_available | boolean | Finance/EMI options available |
virtual_consultation_available | boolean | Telemedicine consultation available |
reference_procedure_id | uuid | Reference to global procedure library for SNOMED CT / CPT codes (optional) |
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). |
FacilityProcedurePricing (Per-Doctor Pricing)
| Field | Type | Description |
|---|---|---|
id | uuid | Unique identifier |
facility_procedure_id | uuid | Reference to catalog procedure |
practitioner_role_id | uuid | Reference to doctor's practitioner role (the catalog.prices.practitioner_id dimension) |
price | decimal | Doctor's price for this procedure |
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_procedure_pricingstable (B.3) and are no longer returned.createon thepricingssub-resource is an upsert on(procedure, role)and takespractitioner_role_id+priceonly.
Associations
| Association | Type | Description |
|---|---|---|
facility | object | Associated facility (id, name) |
reference_procedure | object | Reference procedure from SNOMED CT / CPT global library (optional) |
pricings | array | Per-doctor pricing records |
Endpoints
1. List Facility Procedures
GET /clinical/facilities/:facility_id/facility_procedures
Retrieve all procedures for a facility with filtering and sorting.
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
search_text | string | - | Search by procedure name or category (case-insensitive) |
category | string | - | Filter by procedure category |
include_inactive | boolean | false | Include inactive procedures |
include_pricings | boolean | false | Include per-doctor pricing records for each procedure |
insurance_covered | boolean | - | Filter by insurance coverage availability |
virtual_available | boolean | - | Filter by telemedicine availability |
sort_by_type | string | created_at | Sort field: name, default_price, category, 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_procedures?category=surgery&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": "Appendectomy",
"default_price": 15000.00,
"default_duration": 60,
"category": "surgery",
"is_active": true,
"insurance_covered": true,
"virtual_consultation_available": false,
"pricings": [
{
"id": "018f1234-5678-abcd-ef01-23456789abce",
"facility_procedure_id": "018f1234-5678-abcd-ef01-23456789abcd",
"practitioner_role_id": "018f1234-5678-abcd-ef01-practitioner1",
"price": 12000.00,
"duration": 60,
"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. Search Facility Procedures
GET /clinical/facilities/:facility_id/facility_procedures/search
Quick search endpoint for facility procedures.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
q | string | Search query (searches name and category) |
Request Example
curl -X GET "https://api.kulcare.com/clinical/facilities/{facility_id}/facility_procedures/search?q=append" \
-H "Authorization: Bearer TOKEN"Response Example
{
"data": [
{
"id": "018f1234-5678-abcd-ef01-23456789abcd",
"name": "Appendectomy",
"default_price": 15000.0,
"category": "surgery",
"is_active": true
}
],
"meta": {
"request_id": "req_abc123"
}
}3. Get Facility Procedure
GET /clinical/facilities/:facility_id/facility_procedures/:id
Retrieve a single facility procedure by ID.
Request Example
curl -X GET "https://api.kulcare.com/clinical/facilities/{facility_id}/facility_procedures/{id}" \
-H "Authorization: Bearer TOKEN"Response Example
{
"data": {
"id": "018f1234-5678-abcd-ef01-23456789abcd",
"name": "Appendectomy",
"default_price": 15000.0,
"formatted_default_price": "15000.00",
"default_duration": 60,
"category": "surgery",
"description": "Surgical removal of the appendix",
"is_active": true,
"insurance_covered": true,
"international_options_included": false,
"finance_available": true,
"virtual_consultation_available": false,
"reference_procedure_id": "018f1234-5678-abcd-ef01-snomed-ref",
"created_at": "2025-02-10T10:00:00Z",
"updated_at": "2025-02-10T11:45:00Z",
"facility": {
"id": "018f9876-5432-dcba-fedc-0987654321ab",
"name": "City Health Clinic"
}
},
"meta": {
"request_id": "req_abc123"
}
}4. Create Facility Procedure
POST /clinical/facilities/:facility_id/facility_procedures
Create a new facility procedure (catalog entry). Optionally creates a per-doctor pricing record if practitioner_role_id is provided (for doctor custom entries).
Request Body
{
"facility_procedure": {
"name": "Appendectomy",
"default_price": 15000.0,
"default_duration": 60,
"category": "surgery",
"description": "Surgical removal of the appendix",
"reference_procedure_id": "018f1234-5678-abcd-ef01-snomed-ref",
"is_active": true,
"insurance_covered": true,
"international_options_included": false,
"finance_available": true,
"virtual_consultation_available": false
}
}Fields
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Procedure name (unique per facility, max 255 chars) |
default_price | decimal | No | Default/suggested price (>= 0). Optional — doctors set their own price. |
default_duration | integer | No | Default duration in minutes (> 0) |
category | string | No | Procedure category (max 100 chars) |
description | text | No | Procedure description |
reference_procedure_id | uuid | No | Reference procedure from global SNOMED CT / CPT library |
practitioner_role_id | uuid | No | If provided, auto-creates a pricing record for this doctor (used for custom entries) |
is_active | boolean | No | Active status (default: true) |
insurance_covered | boolean | No | Insurance coverage (default: false) |
international_options_included | boolean | No | International options (default: false) |
finance_available | boolean | No | Finance/EMI available (default: false) |
virtual_consultation_available | boolean | No | Telemedicine available (default: false) |
Request Example
curl -X POST "https://api.kulcare.com/clinical/facilities/{facility_id}/facility_procedures" \
-H "Authorization: Bearer TOKEN" \
-H "Content-Type: application/json" \
-d '{
"facility_procedure": {
"name": "Appendectomy",
"default_price": 15000.0,
"default_duration": 60,
"category": "surgery",
"insurance_covered": true
}
}'Response Example (201 Created)
{
"data": {
"id": "018f1234-5678-abcd-ef01-23456789abcd",
"name": "Appendectomy",
"default_price": 15000.0,
"formatted_default_price": "15000.00",
"default_duration": 60,
"category": "surgery",
"description": "Surgical removal of the appendix",
"is_active": true,
"insurance_covered": true,
"international_options_included": false,
"finance_available": true,
"virtual_consultation_available": false,
"created_at": "2025-02-10T10:00:00Z",
"updated_at": "2025-02-10T11:45: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": "Appendectomy already exists"
}
]
},
"meta": {
"request_id": "req_abc123"
}
}5. Update Facility Procedure
PATCH /clinical/facilities/:facility_id/facility_procedures/:id
Update an existing facility procedure (catalog entry).
Request Body
All fields are optional.
{
"facility_procedure": {
"name": "Laparoscopic Appendectomy",
"default_price": 18000.0,
"is_active": false
}
}Request Example
curl -X PATCH "https://api.kulcare.com/clinical/facilities/{facility_id}/facility_procedures/{id}" \
-H "Authorization: Bearer TOKEN" \
-H "Content-Type: application/json" \
-d '{
"facility_procedure": {
"default_price": 18000.0
}
}'Response Example
{
"data": {
"id": "018f1234-5678-abcd-ef01-23456789abcd",
"name": "Appendectomy",
"default_price": 18000.0,
"formatted_default_price": "18000.00",
"default_duration": 60,
"category": "surgery",
"description": "Surgical removal of the appendix",
"is_active": true,
"insurance_covered": true,
"international_options_included": false,
"finance_available": true,
"virtual_consultation_available": false,
"created_at": "2025-02-10T10:00:00Z",
"updated_at": "2025-02-10T12:30:00Z",
"facility": {
"id": "018f9876-5432-dcba-fedc-0987654321ab",
"name": "City Health Clinic"
}
},
"meta": {
"request_id": "req_abc123"
}
}6. Delete Facility Procedure
DELETE /clinical/facilities/:facility_id/facility_procedures/:id
Delete a facility procedure (catalog entry).
Blocking rule: Cannot delete a procedure 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_procedures/{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 procedure with active pricings. Remove all doctor pricings first.",
"details": [
{
"field": "base",
"message": "Cannot delete procedure with active pricings. Remove all doctor pricings first."
}
]
},
"meta": {
"request_id": "req_abc123"
}
}Per-Doctor Pricing Endpoints
7. List Procedure Pricings
GET /clinical/facilities/:facility_id/facility_procedures/:facility_procedure_id/pricings
Retrieve all per-doctor pricing records for a specific procedure.
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
practitioner_role_id | uuid | - | Filter by specific doctor |
include_inactive | boolean | false | Include inactive pricings |
Response Example
{
"data": [
{
"id": "018f1234-5678-abcd-ef01-23456789abce",
"facility_procedure_id": "018f1234-5678-abcd-ef01-23456789abcd",
"practitioner_role_id": "018f1234-5678-abcd-ef01-practitioner1",
"price": 12000.00,
"duration": 60,
"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"
}
}
}
]
}8. Create Procedure Pricing
POST /clinical/facilities/:facility_id/facility_procedures/:facility_procedure_id/pricings
Create a per-doctor pricing record for a procedure.
Request Body
{
"facility_procedure_pricing": {
"practitioner_role_id": "018f1234-5678-abcd-ef01-practitioner1",
"price": 12000.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) |
9. Update Procedure Pricing
PATCH /clinical/facilities/:facility_id/facility_procedures/:facility_procedure_id/pricings/:id
Update a per-doctor pricing record.
10. Delete Procedure Pricing (Hard Delete)
DELETE /clinical/facilities/:facility_id/facility_procedures/:facility_procedure_id/pricings/:id
Permanently remove a doctor's pricing for this procedure. 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 procedure for a patient, price is resolved as follows:
- Frontend sends
facility_procedure_id+practitioner_id - Backend finds
PractitionerRole.active.find_by(practitioner_id:, facility_id:) - Backend looks up
FacilityProcedurePricing.active.find_by(facility_procedure_id:, practitioner_role_id:) - If pricing found → use
pricing.price(doctor's configured price) - If no pricing → fall back to
facility_procedure.default_price - If neither → reject with error
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
// Facility procedure (catalog entry)
interface FacilityProcedure {
id: string;
name: string;
default_price?: number;
formatted_default_price?: string;
default_duration?: number;
category?: string;
description?: string;
is_active: boolean;
insurance_covered?: boolean;
international_options_included?: boolean;
finance_available?: boolean;
virtual_consultation_available?: boolean;
reference_procedure_id?: string;
created_at: string;
updated_at: string;
pricings?: FacilityProcedurePricing[];
facility?: {
id: string;
name: string;
};
reference_procedure?: {
id: string;
name: string;
};
}
// Per-doctor procedure pricing
interface FacilityProcedurePricing {
id: string;
facility_procedure_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 ListFacilityProceduresParams {
search_text?: string;
category?: string;
include_inactive?: boolean;
include_pricings?: boolean;
insurance_covered?: boolean;
virtual_available?: boolean;
sort_by_type?: 'name' | 'default_price' | 'category' | 'created_at';
sort_by_order?: 'asc' | 'desc';
page?: number;
per_page?: number;
}
// Create request body
interface CreateFacilityProcedureRequest {
facility_procedure: {
name: string;
default_price?: number;
default_duration?: number;
category?: string;
description?: string;
reference_procedure_id?: string;
practitioner_role_id?: string;
is_active?: boolean;
insurance_covered?: boolean;
international_options_included?: boolean;
finance_available?: boolean;
virtual_consultation_available?: boolean;
};
}
// Update request body
interface UpdateFacilityProcedureRequest {
facility_procedure: {
name?: string;
default_price?: number;
default_duration?: number;
category?: string;
description?: string;
reference_procedure_id?: string;
is_active?: boolean;
insurance_covered?: boolean;
international_options_included?: boolean;
finance_available?: boolean;
virtual_consultation_available?: boolean;
};
}
// 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;
};
};
}