Services API Endpoints
Service Catalog
List Services
GET /v1/services?published=true&is_public=true&category=consultationQuery Parameters:
published(boolean) - Filter by published statusis_public(boolean) - Filter patient-visible servicescategory(string) - Filter by category (consultation, therapy, examination, procedure)specialty_id(int) - Filter by specialty
Response:
{
"services": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Mobility Evaluation",
"slug": "mobility-evaluation",
"description": "Comprehensive mobility assessment...",
"category": "consultation",
"specialty": {
"id": 1,
"name": "Physiotherapy"
},
"duration_minutes": 30,
"buffer_minutes": 5,
"base_price": 200.00,
"currency": "RON",
"is_addon": false,
"is_public": true,
"published": true,
"cover_url": "https://...",
"video_url": "https://..."
}
]
}Get Service Details
GET /v1/services/{id}Response:
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Mobility Evaluation",
"slug": "mobility-evaluation",
"description": "...",
"category": "consultation",
"specialty_id": 1,
"duration_minutes": 30,
"buffer_minutes": 5,
"base_price": 200.00,
"currency": "RON",
"is_addon": false,
"is_public": true,
"published": true,
"cover_url": "https://...",
"video_url": "https://...",
"forms": [
{
"form_template_id": 10,
"form_type": "survey",
"sort_order": 1,
"template": {
"id": 10,
"name": "Mobility Survey"
}
}
],
"specialists": [
{
"specialist_id": 1,
"name": "John Doe",
"priority": 1,
"custom_price": null,
"is_active": true
}
],
"attachments": [
{
"id": 42,
"file_url": "https://...",
"file_name": "consent-form.pdf",
"file_type": "application/pdf",
"file_size": 245678
}
]
}Create Service (Admin)
POST /v1/servicesRequest:
{
"name": "Mobility Evaluation",
"slug": "mobility-evaluation",
"description": "Comprehensive mobility assessment",
"category": "consultation",
"specialty_id": 1,
"duration_minutes": 30,
"buffer_minutes": 5,
"base_price": 200.00,
"currency": "RON",
"is_addon": false,
"is_public": true,
"published": true,
"cover_url": "https://...",
"video_url": "https://..."
}Response: Service object (201 Created)
Update Service (Admin)
PUT /v1/services/{id}Request: Same as create (partial updates allowed)
Response: Updated service object
Delete Service (Admin)
DELETE /v1/services/{id}Response: 204 No Content
Service Specialists
List Specialists for Service
GET /v1/services/{id}/specialistsResponse:
{
"specialists": [
{
"specialist_id": 1,
"specialist": {
"id": 1,
"name": "John Doe",
"specialty": "Physiotherapy",
"scheduling_active": true
},
"is_active": true,
"custom_price": null,
"created_at": "2026-02-01T10:00:00Z"
}
]
}Assign Specialist to Service (Admin)
POST /v1/services/{id}/specialistsRequest:
{
"specialist_id": 1,
"is_active": true,
"custom_price": 180.00 // Optional override
}Response: 201 Created
Remove Specialist from Service (Admin)
DELETE /v1/services/{id}/specialists/{specialist_id}Response: 204 No Content
Service Forms
Attach Form Template to Service (Admin)
POST /v1/services/{id}/formsRequest:
{
"form_template_id": 10,
"form_type": "survey",
"sort_order": 1
}Response: 201 Created
Remove Form from Service (Admin)
DELETE /v1/services/{id}/forms/{form_template_id}Response: 204 No Content
Service Attachments
Upload Attachment to Service (Admin)
POST /v1/services/{id}/attachments
Content-Type: multipart/form-dataRequest:
file: [binary]Response:
{
"id": 42,
"service_id": "550e8400-e29b-41d4-a716-446655440000",
"file_url": "services/550e8400.../consent-form.pdf",
"file_name": "consent-form.pdf",
"file_type": "application/pdf",
"file_size": 245678,
"created_at": "2026-02-01T10:00:00Z"
}Delete Attachment (Admin)
DELETE /v1/services/{id}/attachments/{attachment_id}Response: 204 No Content
Service Plans
List Service Plans
GET /v1/service-plans?published=trueQuery Parameters:
published(boolean) - Filter by published statusbilling_model(string) - Filter by billing model (session_based, time_based, hybrid)service_id(uuid) - Filter by linked servicetelerehab_access(boolean) - Filter plans with telerehab accesslibrary_access(boolean) - Filter plans with library access
Response:
{
"service_plans": [
{
"id": 1,
"name": "Kinetotherapy 10 Sessions",
"service": {
"id": "...",
"name": "Kinetotherapy",
"duration_minutes": 30,
"base_price": 100.00
},
"billing_model": "session_based",
"sessions_total": 10,
"validity_days": 90,
"access_months": null,
"telerehab_access": false,
"library_access": false,
"total_price": 1000.00,
"currency": "RON",
"is_published": true,
"bundled_products_count": 0,
"created_at": "2026-02-01T10:00:00Z"
},
{
"id": 2,
"name": "Cervical Recovery Kit",
"service": null,
"billing_model": "time_based",
"sessions_total": null,
"validity_days": null,
"access_months": null,
"telerehab_access": true,
"library_access": true,
"total_price": 89.00,
"currency": "RON",
"is_published": true,
"bundled_products_count": 1,
"created_at": "2026-02-10T10:00:00Z"
}
]
}Get Service Plan Details
GET /v1/service-plans/{id}Response: Service plan object with full service details
Create Service Plan (Admin)
POST /v1/service-plansRequest (session_based):
{
"name": "Kinetotherapy 10 Sessions",
"service_id": "550e8400-e29b-41d4-a716-446655440000",
"billing_model": "session_based",
"sessions_total": 10,
"validity_days": 90,
"total_price": 1000.00,
"is_published": true
}Request (time_based with product bundle):
{
"name": "Cervical Recovery Kit",
"billing_model": "time_based",
"access_months": null,
"telerehab_access": true,
"library_access": true,
"total_price": 89.00,
"is_published": true
}Request (hybrid):
{
"name": "Recovery Package",
"service_id": "550e8400-e29b-41d4-a716-446655440000",
"billing_model": "hybrid",
"sessions_total": 10,
"validity_days": 120,
"access_months": 1,
"telerehab_access": true,
"total_price": 1500.00,
"is_published": true
}Response: Service plan object (201 Created)
Update Service Plan (Admin)
PUT /v1/service-plans/{id}Request: Same as create
Response: Updated service plan object
Delete Service Plan (Admin)
DELETE /v1/service-plans/{id}Response: 204 No Content
Patient Service Plans
List Patient's Enrolled Programs
GET /v1/patient-service-plans?patient_id=123&status=activeQuery Parameters:
patient_id(int) - Filter by patientstatus(string) - Filter by status (active, completed, expired, cancelled)
Response:
{
"patient_service_plans": [
{
"id": 42,
"patient_id": 123,
"service_plan": {
"id": 1,
"name": "Kinetotherapy 10 Sessions",
"billing_model": "session_based",
"telerehab_access": false,
"library_access": false,
"service": {
"name": "Kinetotherapy"
}
},
"sessions_total": 10,
"sessions_completed": 3,
"sessions_cancelled": 0,
"sessions_remaining": 7,
"access_starts_at": null,
"access_expires_at": null,
"status": "active",
"enrolled_at": "2026-02-01T10:00:00Z",
"expires_at": "2026-05-01T10:00:00Z"
}
]
}Get Program Progress
GET /v1/patient-service-plans/{id}Response:
{
"id": 42,
"patient_id": 123,
"patient": {
"id": 123,
"name": "Jane Doe"
},
"service_plan": {
"id": 1,
"name": "Kinetotherapy 10 Sessions",
"service": {
"id": "...",
"name": "Kinetotherapy",
"duration_minutes": 30
},
"total_price": 1000.00
},
"sessions_total": 10,
"sessions_completed": 3,
"sessions_cancelled": 0,
"sessions_remaining": 7,
"status": "active",
"enrolled_at": "2026-02-01T10:00:00Z",
"expires_at": "2026-05-01T10:00:00Z",
"created_at": "2026-02-01T10:00:00Z",
"updated_at": "2026-02-14T15:30:00Z"
}Get Program's Appointment History
GET /v1/patient-service-plans/{id}/appointmentsResponse:
{
"appointments": [
{
"id": 456,
"plan_session_number": 1,
"started_at": "2026-02-05T10:00:00Z",
"status": "done",
"specialist": {
"name": "John Doe"
}
},
{
"id": 457,
"plan_session_number": 2,
"started_at": "2026-02-08T10:00:00Z",
"status": "done",
"specialist": {
"name": "John Doe"
}
},
{
"id": 458,
"plan_session_number": 3,
"started_at": "2026-02-12T10:00:00Z",
"status": "upcoming",
"specialist": {
"name": "John Doe"
}
}
]
}Enroll Patient in Program
POST /v1/patient-service-plansRequest:
{
"patient_id": 123,
"service_plan_id": 1
}Backend:
- Copies
sessions_totalfrom service_plan (frozen at enrollment; NULL for pure time_based) - For session_based: calculates
expires_at = NOW() + service_plan.validity_days - For time_based/hybrid: sets
access_starts_at = NOW(),access_expires_at = NOW() + access_months(NULL access_months = lifetime) - Sets
status = 'active',sessions_completed = 0 - If plan has bundled products (
service_plan_products): auto-createspatient_product_ordersfor each bundled product withstatus = 'pending'
Response:
{
"id": 42,
"patient_id": 123,
"service_plan_id": 1,
"sessions_total": null,
"sessions_completed": 0,
"access_starts_at": "2026-02-17T10:00:00Z",
"access_expires_at": null,
"status": "active",
"enrolled_at": "2026-02-17T10:00:00Z",
"product_orders": [
{
"id": 1,
"product": {
"id": "...",
"name": "Cervical Pillow"
},
"quantity": 1,
"status": "pending"
}
]
}Cancel Program
PATCH /v1/patient-service-plans/{id}/cancelRequest:
{
"reason": "Patient requested cancellation"
}Response: Updated patient service plan with status = 'cancelled'
Products
List Products
GET /v1/products?is_active=trueResponse:
{
"products": [
{
"id": "...",
"name": "Elastic Band",
"slug": "elastic-band",
"description": "Resistance band for exercises",
"price": 40.00,
"currency": "RON",
"is_active": true,
"created_at": "2026-02-01T10:00:00Z"
}
]
}Create Product (Admin)
POST /v1/productsRequest:
{
"name": "Elastic Band",
"slug": "elastic-band",
"description": "Resistance band for exercises",
"price": 40.00,
"currency": "RON",
"is_active": true
}Response: Product object (201 Created)
Update Product (Admin)
PUT /v1/products/{id}Request: Same as create
Response: Updated product object
Delete Product (Admin)
DELETE /v1/products/{id}Response: 204 No Content
Service Plan Bundled Products
List Bundled Products
GET /v1/service-plans/{id}/productsResponse:
{
"bundled_products": [
{
"id": 1,
"product": {
"id": "550e8400-...",
"name": "Cervical Pillow",
"price": 45.00,
"currency": "RON"
},
"quantity": 1,
"created_at": "2026-02-01T10:00:00Z"
}
]
}Add Product to Bundle (Admin)
POST /v1/service-plans/{id}/productsRequest:
{
"product_id": "550e8400-...",
"quantity": 1
}Response: Bundled product object (201 Created)
Update Bundled Product Quantity (Admin)
PUT /v1/service-plans/{id}/products/{product_id}Request:
{
"quantity": 2
}Response: Updated bundled product object
Remove Product from Bundle (Admin)
DELETE /v1/service-plans/{id}/products/{product_id}Response: 204 No Content
Patient Product Orders
List Product Orders
GET /v1/patient-product-orders?patient_id=123&status=pendingQuery Parameters:
patient_id(int) - Filter by patientstatus(string) - Filter by status (pending, confirmed, shipped, delivered, cancelled)source(string) - Filter by source:bundled(has patient_service_plan_id) orstandalone(no plan link)
Response:
{
"product_orders": [
{
"id": 1,
"patient": {
"id": 123,
"name": "Jane Doe"
},
"product": {
"id": "550e8400-...",
"name": "Cervical Pillow",
"price": 45.00
},
"patient_service_plan_id": 42,
"quantity": 1,
"unit_price": null,
"status": "pending",
"ordered_at": "2026-02-17T10:00:00Z",
"shipped_at": null,
"delivered_at": null,
"tracking_number": null
}
]
}Create Standalone Product Order (Specialist, Admin)
POST /v1/patient-product-ordersRequest:
{
"patient_id": 123,
"product_id": "550e8400-...",
"quantity": 1,
"notes": "Recommended during consultation"
}Backend:
- Sets
patient_service_plan_id = NULL(standalone) - Freezes
unit_pricefromproducts.priceat time of order
Response: Product order object (201 Created)
Update Order Status (Specialist, Admin)
PUT /v1/patient-product-orders/{id}/statusRequest:
{
"status": "shipped",
"tracking_number": "RO123456789"
}Valid transitions:
pending→confirmedconfirmed→shipped(requirestracking_number)shipped→deliveredpending→cancelledconfirmed→cancelled
Backend: Sets the corresponding timestamp (confirmed_at, shipped_at, delivered_at)
Response: Updated product order object
Cancel Product Order (Specialist, Admin)
POST /v1/patient-product-orders/{id}/cancelRequest:
{
"reason": "Patient no longer needs the product"
}Response: Updated product order with status = 'cancelled'
Direct Service Registration (No Slot)
For services that don't require slot selection (e.g., "Second Opinion" - patient uploads docs, specialist reviews later):
POST /v1/services/{id}/registerRequest:
{
"contact_name": "John Doe",
"contact_email": "[email protected]",
"contact_phone": "+40123456789"
}Backend:
- Creates appointment with
service_idset,calendar_id = NULL,started_at = NULL - Status:
booked - Customer service later assigns specialist and time, then onboards patient
Response:
{
"appointment_id": 789,
"service": {
"name": "Second Opinion"
},
"status": "booked",
"message": "Thank you! We will contact you shortly."
}