Kulcare Docs
DeveloperFacilities

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.prices in #1838): Each facility has a procedure catalog (facility_procedures) with a default_price. Each doctor (practitioner role) can set their own price. Prices are stored in catalog.prices (the single price store since ADR B.3); the per-doctor row carries a practitioner_id dimension holding the doctor's PractitionerRole id, and the facility default is the practitioner_id NULL row. Billing resolves the doctor's configured price first, falling back to default_price.

Related docs:

  • Procedure Pricing API: pricings endpoints (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)

FieldTypeDescription
iduuidUnique identifier
namestringProcedure name (unique per facility)
default_pricedecimalDefault/suggested price (10,2). Advisory only — billing uses per-doctor pricing.
default_durationintegerDefault duration in minutes (optional)
categorystringProcedure category (optional, max 100 chars)
descriptiontextProcedure description (optional)
is_activebooleanWhether the procedure is active
insurance_coveredbooleanWhether insurance covers this procedure
international_options_includedbooleanInternational patient options available
finance_availablebooleanFinance/EMI options available
virtual_consultation_availablebooleanTelemedicine consultation available
reference_procedure_iduuidReference to global procedure library for SNOMED CT / CPT codes (optional)
created_atdatetimeCreation timestamp
updated_atdatetimeLast update timestamp
formatted_default_pricestringDefault price formatted as string
pricingsarrayPer-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)

FieldTypeDescription
iduuidUnique identifier
facility_procedure_iduuidReference to catalog procedure
practitioner_role_iduuidReference to doctor's practitioner role (the catalog.prices.practitioner_id dimension)
pricedecimalDoctor's price for this procedure
is_activebooleanWhether this pricing is active (listings return active rows only)
practitioner_roleobjectLight embedded role info: { id, display_name }

duration / custom_name were dropped with the old facility_procedure_pricings table (B.3) and are no longer returned. create on the pricings sub-resource is an upsert on (procedure, role) and takes practitioner_role_id + price only.

Associations

AssociationTypeDescription
facilityobjectAssociated facility (id, name)
reference_procedureobjectReference procedure from SNOMED CT / CPT global library (optional)
pricingsarrayPer-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

ParameterTypeDefaultDescription
search_textstring-Search by procedure name or category (case-insensitive)
categorystring-Filter by procedure category
include_inactivebooleanfalseInclude inactive procedures
include_pricingsbooleanfalseInclude per-doctor pricing records for each procedure
insurance_coveredboolean-Filter by insurance coverage availability
virtual_availableboolean-Filter by telemedicine availability
sort_by_typestringcreated_atSort field: name, default_price, category, created_at
sort_by_orderstringdescSort order: asc, desc
pageinteger1Page number
per_pageinteger25Items 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

ParameterTypeDescription
qstringSearch 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

FieldTypeRequiredDescription
namestringYesProcedure name (unique per facility, max 255 chars)
default_pricedecimalNoDefault/suggested price (>= 0). Optional — doctors set their own price.
default_durationintegerNoDefault duration in minutes (> 0)
categorystringNoProcedure category (max 100 chars)
descriptiontextNoProcedure description
reference_procedure_iduuidNoReference procedure from global SNOMED CT / CPT library
practitioner_role_iduuidNoIf provided, auto-creates a pricing record for this doctor (used for custom entries)
is_activebooleanNoActive status (default: true)
insurance_coveredbooleanNoInsurance coverage (default: false)
international_options_includedbooleanNoInternational options (default: false)
finance_availablebooleanNoFinance/EMI available (default: false)
virtual_consultation_availablebooleanNoTelemedicine 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

ParameterTypeDefaultDescription
practitioner_role_iduuid-Filter by specific doctor
include_inactivebooleanfalseInclude 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

FieldTypeRequiredDescription
practitioner_role_iduuidYesDoctor's practitioner role at this facility (must belong to the facility, else 422)
pricedecimalYesDoctor'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:

  1. Frontend sends facility_procedure_id + practitioner_id
  2. Backend finds PractitionerRole.active.find_by(practitioner_id:, facility_id:)
  3. Backend looks up FacilityProcedurePricing.active.find_by(facility_procedure_id:, practitioner_role_id:)
  4. If pricing found → use pricing.price (doctor's configured price)
  5. If no pricing → fall back to facility_procedure.default_price
  6. If neither → reject with error

See docs/brainstorms/2026-06-08-billing-packages-problem-and-solution.md for full billing flow.


Error Codes

CodeDescription
not_foundResource not found
validation_errorRequest validation failed
unauthorizedAuthentication 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;
    };
  };
}

On this page