Skip to content

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

json
{
  "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:

json
{
  "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

json
{
  "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 upcoming status (patient already exists)
  • Does not auto-generate forms (use /attach-forms endpoint)
  • Creates videocall room automatically

POST /v1/appointments/from-template

Create appointment from template with all forms auto-generated and videocall room.

Request:

json
{
  "template_id": 5,
  "user_id": 42,
  "specialist_id": 2,
  "started_at": "2025-01-20T10:00:00Z"
}

Response: 201

json
{
  "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:

  1. Fetch template with all form template relations
  2. Calculate ended_at from template duration
  3. Create appointment record
  4. Generate all form instances from template's form templates
  5. Link forms to appointment via appointment_id FK
  6. Create Daily.co videocall room
  7. Return complete appointment

POST /v1/appointments/{id}/attach-forms

Add forms to an existing appointment that was created without them.

Request:

json
{
  "template_id": 5
}

Response: 200

json
{
  "data": {
    "id": 102,
    "forms": {
      "surveys": [...],
      "disclaimers": [...]
    }
  }
}

GET /v1/appointments/uid/{uid}

Find appointment by UUID. Ownership validated by RLS.

Response: 200

json
{
  "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:

json
{
  "title": "Updated Title",
  "started_at": "2025-01-25T14:00:00Z",
  "ended_at": "2025-01-25T14:30:00Z"
}

Notes:

  • Cannot update status directly (use /status endpoint)
  • Specialist and admin only
  • Updates videocall room expiration if times changed

DELETE /v1/appointments/{id}

Soft-delete an appointment. Admin only.

Response: 200

json
{
  "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:

json
{
  "status": "inprogress"
}

Response: 200

json
{
  "data": {
    "id": 102,
    "status": "inprogress",
    "previous_status": "confirmed",
    "updated_at": "2025-01-20T10:00:00Z"
  }
}

Error: 400 (invalid transition)

json
{
  "error": {
    "code": "invalid_transition",
    "message": "cannot transition from \"done\" to \"upcoming\" as \"specialist\"",
    "details": {
      "from": "done",
      "to": "upcoming",
      "role": "specialist"
    }
  }
}

Side effects by transition:

TransitionSide Effects
booked → upcomingOnboard patient, generate forms, create videocall room
booked → cancelledClear rate limit, emit webhook
* → cancelledDelete videocall room, emit webhook
cancelled → upcomingRecreate videocall room
* → noshowDelete videocall room
noshow → upcomingRecreate videocall room
upcoming → inprogress
inprogress → doneCheck 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:

json
{
  "started_at": "2025-01-25T14:00:00Z",
  "ended_at": "2025-01-25T14:30:00Z"
}

Response: 200

json
{
  "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, or confirmed
  • started_at must 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

json
{
  "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 booked status, clear rate limit for booking client
  • Emit appointment.cancelled webhook

POST /v1/appointments/{id}/onboard

Onboard a booked appointment (convert contact info into patient account and generate forms).

Request: (empty body)

Response: 200

json
{
  "data": {
    "id": 102,
    "status": "upcoming",
    "patient_id": 100,
    "forms": [...],
    "videocall_room": "restartix-102"
  }
}

Business logic:

  1. Load appointment (must be status booked)
  2. Find or create user by contact_email
  3. Find or create patient record
  4. Link patient to appointment
  5. Generate forms from appointment template
  6. Create videocall room
  7. Transition status: booked → upcoming

Errors:

  • 400 invalid_status - Appointment is not booked
  • 400 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:

json
{
  "view": "month",
  "start_date": "2025-01-01",
  "end_date": "2025-01-31",
  "specialist_id": 2,
  "status": ["upcoming", "confirmed", "done"]
}

Response: 200 (month view)

json
{
  "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)

json
{
  "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

CodeHTTPDescription
appointment_not_found404Appointment doesn't exist or not accessible
invalid_transition400Status transition not allowed
invalid_status400Appointment status doesn't match requirement
already_cancelled409Appointment is already cancelled
appointment_in_past400Cannot reschedule to past date
late_cancellation_restricted403Patient cannot cancel < 24 hours before
missing_contact_info400Booking has no contact email
template_not_found404Appointment template doesn't exist
videocall_room_error502Daily.co API error