Appointments API
All appointments endpoints follow the global API conventions.
Base Endpoints
GET /v1/appointments
List appointments. Automatically filtered by RLS (patients see own, specialists see assigned, admins see all in org).
Query params:
?status=upcoming,confirmed- Filter by status (comma-separated)?specialist_id=5- Filter by specialist?started_at.gte=2025-01-01T00:00:00Z- Start date range (greater than or equal)?started_at.lte=2025-01-31T23:59:59Z- Start date range (less than or equal)?sort=started_at- Sort by field (prefix with-for descending)?include=specialist,user,appointment_template- Include relations
Response: 200
{
"data": [
{
"id": 101,
"uid": "550e8400-e29b-41d4-a716-446655440000",
"title": "Initial Consultation",
"status": "upcoming",
"started_at": "2025-01-20T10:00:00Z",
"ended_at": "2025-01-20T10:30:00Z",
"specialist": {
"id": 2,
"name": "Dr. Smith",
"avatar_url": "https://s3.../avatar.jpg"
},
"organization_id": 1,
"created_at": "2025-01-15T09:00:00Z"
}
],
"meta": { "page": 1, "page_size": 25, "total": 3, "total_pages": 1 }
}POST /v1/appointments
Create a basic appointment (no forms generated). Used for manual appointment creation by staff.
Request:
{
"title": "Follow-up",
"user_id": 42,
"specialist_id": 2,
"specialty_id": 1,
"appointment_template_id": 5,
"started_at": "2025-01-20T10:00:00Z",
"ended_at": "2025-01-20T10:30:00Z"
}Response: 201
{
"data": {
"id": 102,
"uid": "...",
"title": "Follow-up",
"status": "upcoming",
"started_at": "2025-01-20T10:00:00Z",
"ended_at": "2025-01-20T10:30:00Z",
"specialist_id": 2,
"user_id": 42
}
}Notes:
- Creates appointment in
upcomingstatus (patient already exists) - Does not auto-generate forms (use
/attach-formsendpoint) - Creates videocall room automatically
POST /v1/appointments/from-template
Create appointment from template with all forms auto-generated and videocall room.
Request:
{
"template_id": 5,
"user_id": 42,
"specialist_id": 2,
"started_at": "2025-01-20T10:00:00Z"
}Response: 201
{
"data": {
"id": 102,
"uid": "...",
"title": "Initial Consultation",
"status": "upcoming",
"started_at": "2025-01-20T10:00:00Z",
"ended_at": "2025-01-20T10:30:00Z",
"specialist_id": 2,
"forms": {
"surveys": [{ "id": 201, "title": "Patient Intake Survey" }],
"disclaimers": [{ "id": 202, "title": "Consent Form" }],
"parameters": { "id": 203, "title": "Vital Signs" },
"analysis": { "id": 204, "title": "Specialist Analysis" },
"advice": { "id": 205, "title": "Treatment Advice" }
},
"videocall_room": "restartix-102"
}
}Business logic:
- Fetch template with all form template relations
- Calculate
ended_atfrom template duration - Create appointment record
- Generate all form instances from template's form templates
- Link forms to appointment via
appointment_idFK - Create Daily.co videocall room
- Return complete appointment
POST /v1/appointments/{id}/attach-forms
Add forms to an existing appointment that was created without them.
Request:
{
"template_id": 5
}Response: 200
{
"data": {
"id": 102,
"forms": {
"surveys": [...],
"disclaimers": [...]
}
}
}GET /v1/appointments/uid/{uid}
Find appointment by UUID. Ownership validated by RLS.
Response: 200
{
"data": {
"id": 102,
"uid": "550e8400-e29b-41d4-a716-446655440000",
"title": "Initial Consultation",
"status": "upcoming",
"started_at": "2025-01-20T10:00:00Z",
"specialist": { "id": 2, "name": "Dr. Smith" },
"forms": { ... },
"report": null,
"prescription": null,
"custom_field_values": [ ... ]
}
}GET /v1/appointments/{id}
Get appointment details. Same response format as /uid/{uid}.
PUT /v1/appointments/{id}
Update appointment fields (title, times, etc.).
Request:
{
"title": "Updated Title",
"started_at": "2025-01-25T14:00:00Z",
"ended_at": "2025-01-25T14:30:00Z"
}Notes:
- Cannot update
statusdirectly (use/statusendpoint) - Specialist and admin only
- Updates videocall room expiration if times changed
DELETE /v1/appointments/{id}
Soft-delete an appointment. Admin only.
Response: 200
{
"data": {
"id": 102,
"deleted_at": "2025-01-15T10:00:00Z"
}
}Status Transitions
PUT /v1/appointments/{id}/status
Transition appointment status with validation and side effects.
Request:
{
"status": "inprogress"
}Response: 200
{
"data": {
"id": 102,
"status": "inprogress",
"previous_status": "confirmed",
"updated_at": "2025-01-20T10:00:00Z"
}
}Error: 400 (invalid transition)
{
"error": {
"code": "invalid_transition",
"message": "cannot transition from \"done\" to \"upcoming\" as \"specialist\"",
"details": {
"from": "done",
"to": "upcoming",
"role": "specialist"
}
}
}Side effects by transition:
| Transition | Side Effects |
|---|---|
booked → upcoming | Onboard patient, generate forms, create videocall room |
booked → cancelled | Clear rate limit, emit webhook |
* → cancelled | Delete videocall room, emit webhook |
cancelled → upcoming | Recreate videocall room |
* → noshow | Delete videocall room |
noshow → upcoming | Recreate videocall room |
upcoming → inprogress | — |
inprogress → done | Check form completion |
Audit: All status transitions (and all other mutations) are automatically captured by the audit middleware. Each transition generates an audit_log entry (synchronous local write) that is then forwarded to Telemetry asynchronously for enrichment. No feature code needed — see ../audit/README.md.
See lifecycle.md for complete state machine documentation.
Scheduling Operations
POST /v1/appointments/{id}/reschedule
Reschedule an appointment. Updates videocall room expiry.
Request:
{
"started_at": "2025-01-25T14:00:00Z",
"ended_at": "2025-01-25T14:30:00Z"
}Response: 200
{
"data": {
"id": 102,
"started_at": "2025-01-25T14:00:00Z",
"ended_at": "2025-01-25T14:30:00Z",
"updated_at": "2025-01-15T10:00:00Z"
}
}Validation:
- Status must be
booked,upcoming, orconfirmed started_atmust be in the future- Patient can only reschedule > 24 hours before start (configurable)
- Specialists and admins can reschedule at any time
POST /v1/appointments/{id}/cancel
Cancel an appointment.
Response: 200
{
"data": {
"id": 102,
"status": "cancelled",
"message": "Appointment cancelled successfully"
}
}Validation:
- Patient can cancel > 24 hours before start (configurable)
- Specialists and admins can cancel at any time
- Late cancellations (< 24 hours) are logged with flag
Side effects:
- Delete Daily.co room
- If
bookedstatus, clear rate limit for booking client - Emit
appointment.cancelledwebhook
POST /v1/appointments/{id}/onboard
Onboard a booked appointment (convert contact info into patient account and generate forms).
Request: (empty body)
Response: 200
{
"data": {
"id": 102,
"status": "upcoming",
"patient_id": 100,
"forms": [...],
"videocall_room": "restartix-102"
}
}Business logic:
- Load appointment (must be status
booked) - Find or create user by
contact_email - Find or create patient record
- Link patient to appointment
- Generate forms from appointment template
- Create videocall room
- Transition status:
booked → upcoming
Errors:
400 invalid_status- Appointment is notbooked400 missing_contact_info- No contact email on appointment
Calendar Endpoints
GET /v1/calendar
POST /v1/calendar
Get calendar view data for appointments. Supports both GET (query params) and POST (body) for complex filters.
Query/Body params:
{
"view": "month",
"start_date": "2025-01-01",
"end_date": "2025-01-31",
"specialist_id": 2,
"status": ["upcoming", "confirmed", "done"]
}Response: 200 (month view)
{
"data": {
"view": "month",
"days": [
{ "date": "2025-01-15", "count": 3 },
{ "date": "2025-01-16", "count": 1 },
{ "date": "2025-01-20", "count": 5 }
]
}
}Response: 200 (week view)
{
"data": {
"view": "week",
"days": [
{
"date": "2025-01-20",
"appointments": [
{
"id": 102,
"title": "Initial Consultation",
"started_at": "2025-01-20T10:00:00Z",
"ended_at": "2025-01-20T10:30:00Z",
"status": "upcoming",
"specialist": { "id": 2, "name": "Dr. Smith" },
"person": { "id": 81, "name": "John Doe", "username": "[email protected]" }
}
]
}
]
}
}See calendar.md for view logic details.
Error Codes
| Code | HTTP | Description |
|---|---|---|
appointment_not_found | 404 | Appointment doesn't exist or not accessible |
invalid_transition | 400 | Status transition not allowed |
invalid_status | 400 | Appointment status doesn't match requirement |
already_cancelled | 409 | Appointment is already cancelled |
appointment_in_past | 400 | Cannot reschedule to past date |
late_cancellation_restricted | 403 | Patient cannot cancel < 24 hours before |
missing_contact_info | 400 | Booking has no contact email |
template_not_found | 404 | Appointment template doesn't exist |
videocall_room_error | 502 | Daily.co API error |