Skip to content

Treatment Plans API Endpoints

Treatment Plan CRUD

List Treatment Plans

http
GET /v1/treatment-plans?type=telerehab&status=active&scope=org&specialist_id=5&sort=-created_at&page=1&limit=25

Query Parameters:

  • type (string) — Filter by type (telerehab, in_clinic)
  • status (string) — Filter by status (draft, pending_approval, active, paused, completed, cancelled, expired)
  • scope (string) — Filter by scope: global (org_id IS NULL), org (org_id = current), custom (created_for_patient_id IS NOT NULL), all (default)
  • visibility (string) — Filter by visibility: internal, library
  • specialist_id (int) — Filter by prescribing specialist
  • specialty_id (int) — Filter by specialty
  • condition_tags (string[]) — Filter by condition tags (e.g., ?condition_tags=shoulder_pain,post_acl)
  • published (boolean) — Filter by published status
  • q (string) — Search by name/description
  • Standard pagination: page, limit, sort

Response:

json
{
  "treatment_plans": [
    {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "name": "Post-ACL Rehab Weeks 1-4",
      "description": "Progressive rehabilitation program for ACL reconstruction recovery",
      "type": "telerehab",
      "scope": "org",
      "visibility": "library",
      "condition_tags": ["post_acl", "knee_rehab"],
      "created_from_id": null,
      "created_for_patient_id": null,
      "frequency_per_week": 3,
      "duration_weeks": 4,
      "total_sessions": 12,
      "specialist": {
        "id": 5,
        "name": "Dr. Maria Kent"
      },
      "specialty": {
        "id": 1,
        "name": "Physiotherapy"
      },
      "requires_approval": true,
      "post_session_form_template_id": 42,
      "service_plan_id": null,
      "status": "active",
      "version": 2,
      "published": true,
      "sessions_count": 3,
      "created_at": "2026-02-01T10:00:00Z",
      "updated_at": "2026-02-14T15:30:00Z"
    }
  ],
  "total": 8,
  "page": 1,
  "limit": 25,
  "total_pages": 1
}

Get Treatment Plan Details

http
GET /v1/treatment-plans/{id}

Response:

json
{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "name": "Post-ACL Rehab Weeks 1-4",
  "description": "Progressive rehabilitation program...",
  "type": "telerehab",
  "frequency_per_week": 3,
  "duration_weeks": 4,
  "total_sessions": 12,
  "specialist": {
    "id": 5,
    "name": "Dr. Maria Kent"
  },
  "specialty": {
    "id": 1,
    "name": "Physiotherapy"
  },
  "requires_approval": true,
  "post_session_form_template": {
    "id": 42,
    "title": "Pain & Mobility Questionnaire"
  },
  "service_plan": null,
  "status": "active",
  "version": 2,
  "published": true,
  "sessions": [
    {
      "id": 1,
      "session_number": 1,
      "name": "Mobility Focus",
      "description": "Gentle mobility exercises to restore range of motion",
      "estimated_duration_minutes": 25,
      "exercises": [
        {
          "id": 10,
          "exercise_id": "660e8400-...",
          "sort_order": 0,
          "exercise": {
            "id": "660e8400-...",
            "name": "Quad Set",
            "video_thumbnail_url": "https://...",
            "difficulty": "beginner"
          },
          "mode": "reps",
          "sets": 3,
          "reps": 10,
          "hold_seconds": null,
          "duration_seconds": null,
          "rest_between_sets_seconds": 30,
          "rest_after_exercise_seconds": 60,
          "notes": "Focus on controlled contraction"
        },
        {
          "id": 11,
          "exercise_id": "770e8400-...",
          "sort_order": 1,
          "exercise": {
            "id": "770e8400-...",
            "name": "Prone Hang",
            "video_thumbnail_url": "https://...",
            "difficulty": "beginner"
          },
          "mode": "duration",
          "sets": 2,
          "reps": null,
          "hold_seconds": 60,
          "duration_seconds": 60,
          "rest_between_sets_seconds": 45,
          "rest_after_exercise_seconds": 60,
          "notes": "Let gravity extend the knee"
        }
      ]
    },
    {
      "id": 2,
      "session_number": 2,
      "name": "Strengthening Focus",
      "description": "Progressive strengthening exercises",
      "estimated_duration_minutes": 30,
      "exercises": []
    }
  ],
  "created_by_user_id": 1,
  "created_at": "2026-02-01T10:00:00Z",
  "updated_at": "2026-02-14T15:30:00Z"
}

Create Treatment Plan

http
POST /v1/treatment-plans

Request:

json
{
  "name": "Post-ACL Rehab Weeks 1-4",
  "description": "Progressive rehabilitation program for ACL reconstruction recovery",
  "type": "telerehab",
  "frequency_per_week": 3,
  "duration_weeks": 4,
  "specialist_id": 5,
  "specialty_id": 1,
  "requires_approval": true,
  "post_session_form_template_id": 42,
  "service_plan_id": null
}

Backend:

  • Creates plan in draft status
  • Sets version = 1, published = false
  • Validates specialist and specialty exist in org

Response: Treatment plan object (201 Created)

Update Treatment Plan

http
PUT /v1/treatment-plans/{id}

Request: Same as create (partial updates allowed)

Validation:

  • Can only update plans in draft status
  • To modify a published plan: the update creates a new draft version on the existing plan

Response: Updated treatment plan object

Soft Delete Treatment Plan

http
DELETE /v1/treatment-plans/{id}

Backend:

  • Sets deleted_at = NOW()
  • Active patient enrollments are NOT cancelled (they continue on their assigned version)

Response: 204 No Content


Treatment Plan Publishing & Versioning

Publish Treatment Plan

http
POST /v1/treatment-plans/{id}/publish

Backend:

  1. Validates plan has at least 1 session with at least 1 exercise
  2. Creates a treatment_plan_versions record with full JSONB snapshot
  3. Increments treatment_plans.version
  4. Sets treatment_plans.published = true
  5. If requires_approval = false: sets status = 'active'
  6. If requires_approval = true: sets status = 'pending_approval'

Response:

json
{
  "id": "550e8400-...",
  "version": 2,
  "status": "active",
  "published": true,
  "published_at": "2026-02-14T15:30:00Z"
}

List Version History

http
GET /v1/treatment-plans/{id}/versions

Response:

json
{
  "versions": [
    {
      "id": 2,
      "version": 2,
      "name": "Post-ACL Rehab Weeks 1-4",
      "type": "telerehab",
      "frequency_per_week": 3,
      "duration_weeks": 4,
      "sessions_count": 3,
      "created_by_user_id": 1,
      "created_at": "2026-02-14T15:30:00Z"
    },
    {
      "id": 1,
      "version": 1,
      "name": "Post-ACL Rehab Weeks 1-4",
      "type": "telerehab",
      "frequency_per_week": 3,
      "duration_weeks": 2,
      "sessions_count": 2,
      "created_by_user_id": 1,
      "created_at": "2026-02-01T10:00:00Z"
    }
  ]
}

Get Specific Version

http
GET /v1/treatment-plans/{id}/versions/{version}

Response: Full version object with sessions_snapshot JSONB

Rollback to Version

http
POST /v1/treatment-plans/{id}/rollback/{version}

Backend:

  • Restores sessions from the version snapshot
  • Creates a NEW version (increments version counter)
  • Does not delete the current version

Response: Updated treatment plan object with new version number

Clone Treatment Plan

http
POST /v1/treatment-plans/{id}/clone

Request:

json
{
  "name": "Jane Doe - Post-ACL Rehab Weeks 1-4",
  "created_for_patient_id": 123
}

Backend:

  • Copies plan, sessions, and session exercises into the current org
  • Sets created_from_id to the source plan's ID (tracks lineage)
  • If created_for_patient_id provided: creates as custom (specialist-for-patient)
  • Resets to draft status, version = 1, visibility = 'internal'
  • Works across scopes: clone global → org, clone org → custom, clone org → org

Permissions: admin, specialist

Response: New treatment plan object (201 Created)

Promote Treatment Plan

http
POST /v1/treatment-plans/{id}/promote

Request:

json
{
  "visibility": "library"
}

Backend:

  • Clears created_for_patient_id (makes plan reusable, no longer patient-specific)
  • Optionally sets visibility to 'library' (patient-browseable) or keeps 'internal'
  • Plan stays in the same org
  • Only works on custom plans (created_for_patient_id IS NOT NULL)

Permissions: admin only

Response: Updated treatment plan object


Treatment Plan Library (Patient-Facing)

Browse Library

http
GET /v1/treatment-plans/library?condition_tags=shoulder_pain&type=telerehab&sort=-created_at&page=1&limit=25

Query Parameters:

  • condition_tags (string[]) — Filter by condition/pain area
  • type (string) — Filter by type (telerehab, in_clinic)
  • specialty_id (int) — Filter by specialty
  • q (string) — Search by name/description
  • Standard pagination

Backend:

  • Returns plans where published = TRUE AND visibility = 'library'
  • Includes both global plans (organization_id IS NULL) and org plans (organization_id = current_org)
  • RLS handles scope automatically

Permissions: patient with active service plan that has library_access = TRUE

Response:

json
{
  "treatment_plans": [
    {
      "id": "550e8400-...",
      "name": "Post-ACL Rehab Weeks 1-4",
      "description": "Progressive rehabilitation program...",
      "type": "telerehab",
      "scope": "global",
      "condition_tags": ["post_acl", "knee_rehab"],
      "frequency_per_week": 3,
      "duration_weeks": 4,
      "total_sessions": 12,
      "specialty": {
        "id": 1,
        "name": "Physiotherapy"
      },
      "sessions_count": 3,
      "difficulty_level": "beginner"
    }
  ],
  "total": 24,
  "page": 1,
  "limit": 25,
  "total_pages": 1
}

Self-Assign Plan

http
POST /v1/treatment-plans/{id}/self-assign

Request:

json
{
  "start_date": "2026-02-17"
}

Backend:

  1. Verifies patient has active patient_service_plan with library_access = TRUE
  2. Verifies plan is published = TRUE AND visibility = 'library'
  3. Creates patient_treatment_plan with:
    • self_assigned = TRUE
    • assigned_by_specialist_id = NULL
    • status = 'active' (no approval needed for self-assigned)
    • Latest published version frozen
  4. Links to patient's active patient_service_plan_id
  5. Fires treatment_plan.assigned automation trigger

Permissions: patient (own only, with library access)

Response: Patient treatment plan object (201 Created)


Treatment Plan Sessions

List Sessions

http
GET /v1/treatment-plans/{id}/sessions

Response:

json
{
  "sessions": [
    {
      "id": 1,
      "session_number": 1,
      "name": "Mobility Focus",
      "description": "Gentle mobility exercises",
      "estimated_duration_minutes": 25,
      "exercises_count": 4
    }
  ]
}

Add Session

http
POST /v1/treatment-plans/{id}/sessions

Request:

json
{
  "session_number": 3,
  "name": "Balance & Control",
  "description": "Balance and proprioception exercises",
  "estimated_duration_minutes": 20
}

Validation: Plan must be in draft status

Response: Session object (201 Created)

Update Session

http
PUT /v1/treatment-plans/{planId}/sessions/{sessionId}

Response: Updated session object

Delete Session

http
DELETE /v1/treatment-plans/{planId}/sessions/{sessionId}

Backend: Cascades to session exercises. Renumbers subsequent sessions.

Response: 204 No Content

Reorder Sessions

http
PUT /v1/treatment-plans/{planId}/sessions/reorder

Request:

json
{
  "session_ids": [3, 1, 2]
}

Response: Updated sessions list with new session_number values


Session Exercises

List Exercises in Session

http
GET /v1/treatment-plan-sessions/{sessionId}/exercises

Response:

json
{
  "exercises": [
    {
      "id": 10,
      "exercise_id": "660e8400-...",
      "sort_order": 0,
      "exercise": {
        "id": "660e8400-...",
        "name": "Quad Set",
        "video_url": "https://...",
        "video_thumbnail_url": "https://...",
        "difficulty": "beginner",
        "estimated_duration_seconds": 45
      },
      "mode": "reps",
      "sets": 3,
      "reps": 10,
      "hold_seconds": null,
      "duration_seconds": null,
      "rest_between_sets_seconds": 30,
      "rest_after_exercise_seconds": 60,
      "notes": "Focus on controlled contraction"
    }
  ]
}

Add Exercise to Session

http
POST /v1/treatment-plan-sessions/{sessionId}/exercises

Request:

json
{
  "exercise_id": "660e8400-...",
  "sort_order": 0,
  "mode": "reps",
  "sets": 3,
  "reps": 10,
  "rest_between_sets_seconds": 30,
  "rest_after_exercise_seconds": 60,
  "notes": "Focus on controlled contraction"
}

Validation:

  • Exercise must exist and be published (or draft for admin)
  • Plan must be in draft status
  • Validates mode-specific fields (reps requires reps, duration requires hold_seconds/duration_seconds)

Response: Session exercise object (201 Created)

Update Exercise Config

http
PUT /v1/treatment-plan-sessions/{sessionId}/exercises/{exerciseId}

Request: Same as add (partial updates allowed)

Response: Updated session exercise object

Remove Exercise from Session

http
DELETE /v1/treatment-plan-sessions/{sessionId}/exercises/{exerciseId}

Response: 204 No Content

Reorder Exercises

http
PUT /v1/treatment-plan-sessions/{sessionId}/exercises/reorder

Request:

json
{
  "exercise_ids": [11, 10, 12]
}

Response: Updated exercises list with new sort_order values


Patient Treatment Plans (Enrollment)

List Patient Enrollments

http
GET /v1/patient-treatment-plans?patient_id=123&status=active

Query Parameters:

  • patient_id (int) — Filter by patient
  • treatment_plan_id (uuid) — Filter by plan
  • status (string) — Filter by status
  • type (string) — Filter by plan type (telerehab, in_clinic)
  • Standard pagination

Response:

json
{
  "patient_treatment_plans": [
    {
      "id": 42,
      "patient": {
        "id": 123,
        "name": "Jane Doe"
      },
      "treatment_plan": {
        "id": "550e8400-...",
        "name": "Post-ACL Rehab Weeks 1-4",
        "type": "telerehab"
      },
      "treatment_plan_version": 2,
      "assigned_by_specialist": {
        "id": 5,
        "name": "Dr. Maria Kent"
      },
      "start_date": "2026-02-10",
      "end_date": "2026-03-10",
      "frequency_per_week": 3,
      "sessions_total": 12,
      "sessions_completed": 5,
      "sessions_skipped": 1,
      "completion_percentage": 41.67,
      "status": "active",
      "approved_at": "2026-02-10T10:00:00Z",
      "created_at": "2026-02-09T14:00:00Z"
    }
  ]
}

Assign Plan to Patient

http
POST /v1/patient-treatment-plans

Request:

json
{
  "patient_id": 123,
  "treatment_plan_id": "550e8400-...",
  "start_date": "2026-02-10",
  "assigned_by_specialist_id": 5,
  "patient_service_plan_id": null
}

Backend:

  • Verifies patient has active patient_service_plan with telerehab_access = TRUE (for telerehab plans)
  • Assigns the latest published version of the plan
  • Freezes sessions_total, frequency_per_week from plan version
  • Calculates end_date = start_date + (duration_weeks * 7)
  • Sets self_assigned = FALSE
  • If treatment_plan.requires_approval = true: sets status = 'pending_approval'
  • If treatment_plan.requires_approval = false: sets status = 'active'
  • Fires treatment_plan.assigned automation trigger

Response: Patient treatment plan object (201 Created)

Get Enrollment Details

http
GET /v1/patient-treatment-plans/{id}

Response: Full enrollment object with plan details, progress, and session history

Approve Patient Plan

http
POST /v1/patient-treatment-plans/{id}/approve

Backend:

  • Validates status = 'pending_approval'
  • Sets status = 'active', approved_at = NOW(), approved_by_user_id = current user
  • Fires treatment_plan.activated automation trigger

Response: Updated enrollment object

Change Enrollment Status

http
PUT /v1/patient-treatment-plans/{id}/status

Request:

json
{
  "status": "paused",
  "reason": "Patient traveling for 2 weeks"
}

Allowed transitions: See lifecycle.md

Response: Updated enrollment object

List Patient's Plans

http
GET /v1/patients/{patientId}/treatment-plans?status=active

Convenience endpoint for viewing all plans for a specific patient.

Response: Same as list patient enrollments, filtered by patient

Get Today's Session

http
GET /v1/patients/{patientId}/treatment-plans/today

Backend:

  • Finds active patient_treatment_plans for the patient
  • Determines which session template to present next based on:
    • Sessions already completed
    • Current week number
    • Plan rotation pattern (sessions cycle: session 1, 2, 3, 1, 2, 3, ...)
  • Returns the session template with exercises and full details

Response:

json
{
  "patient_treatment_plan_id": 42,
  "treatment_plan": {
    "id": "550e8400-...",
    "name": "Post-ACL Rehab Weeks 1-4"
  },
  "next_session": {
    "session_number": 2,
    "name": "Strengthening Focus",
    "estimated_duration_minutes": 30,
    "exercises": [
      {
        "exercise_id": "770e8400-...",
        "exercise": {
          "name": "Straight Leg Raise",
          "video_url": "https://...",
          "video_thumbnail_url": "https://...",
          "instructions": [...]
        },
        "mode": "reps",
        "sets": 3,
        "reps": 10,
        "rest_between_sets_seconds": 30,
        "rest_after_exercise_seconds": 60,
        "notes": "Keep knee fully extended"
      }
    ]
  },
  "progress": {
    "sessions_completed": 5,
    "sessions_total": 12,
    "current_week": 2,
    "total_weeks": 4,
    "completion_percentage": 41.67
  }
}

Patient Session Execution

Start Session

http
POST /v1/patient-treatment-plans/{id}/sessions/{sessionNumber}/start

Request:

json
{
  "pain_level_before": 6
}

Backend:

  • Creates patient_session_completions record with status = 'in_progress'
  • Sets started_at = NOW()
  • Sets exercises_total from session template
  • Pre-creates patient_exercise_logs entries for each exercise (with prescribed params)

Response:

json
{
  "session_completion_id": 789,
  "session_number": 2,
  "status": "in_progress",
  "started_at": "2026-02-16T09:00:00Z",
  "exercises_total": 3,
  "exercises": [
    {
      "exercise_log_id": 100,
      "exercise_id": "770e8400-...",
      "sort_order": 0,
      "exercise": {
        "name": "Straight Leg Raise",
        "video_url": "https://...",
        "instructions": [...]
      },
      "prescribed_mode": "reps",
      "prescribed_sets": 3,
      "prescribed_reps": 10,
      "rest_between_sets_seconds": 30,
      "rest_after_exercise_seconds": 60,
      "notes": "Keep knee fully extended"
    }
  ]
}

Update Exercise Log (During Session)

http
PUT /v1/patient-exercise-logs/{id}

Request:

json
{
  "actual_sets": 3,
  "actual_reps": 10,
  "video_watched": true,
  "video_watch_seconds": 38,
  "video_watch_percentage": 90.48,
  "started_at": "2026-02-16T09:02:00Z",
  "completed_at": "2026-02-16T09:05:30Z"
}

Response: Updated exercise log object

Skip Exercise

http
PUT /v1/patient-exercise-logs/{id}

Request:

json
{
  "skipped": true,
  "skip_reason": "Too much pain in left knee"
}

Response: Updated exercise log with skipped = true

Complete Session

http
POST /v1/patient-session-completions/{id}/complete

Request:

json
{
  "pain_level_after": 4,
  "perceived_difficulty": 3,
  "notes": "Felt good overall, mild discomfort in last exercise"
}

Backend:

  1. Sets status = 'completed', completed_at = NOW()
  2. Calculates duration_seconds from started_at to now
  3. Counts exercises_completed and exercises_skipped from exercise logs
  4. Increments patient_treatment_plans.sessions_completed
  5. If sessions_completed >= sessions_total: transitions enrollment to completed
  6. Creates post-session form instance from post_session_form_template_id (if configured)
  7. Fires treatment_plan.session_completed automation trigger
  8. If plan completed: fires treatment_plan.completed automation trigger

Response:

json
{
  "id": 789,
  "session_number": 2,
  "status": "completed",
  "started_at": "2026-02-16T09:00:00Z",
  "completed_at": "2026-02-16T09:28:00Z",
  "duration_seconds": 1680,
  "exercises_total": 3,
  "exercises_completed": 3,
  "exercises_skipped": 0,
  "pain_level_before": 6,
  "pain_level_after": 4,
  "perceived_difficulty": 3,
  "post_session_form_id": 456,
  "plan_progress": {
    "sessions_completed": 6,
    "sessions_total": 12,
    "completion_percentage": 50.0
  }
}

Skip Session

http
POST /v1/patient-session-completions/{id}/skip

Request:

json
{
  "reason": "Not feeling well today"
}

Backend:

  • Sets status = 'skipped'
  • Increments patient_treatment_plans.sessions_skipped
  • Does NOT increment sessions_completed

Response: Updated session completion object

Get Session Completion Details

http
GET /v1/patient-session-completions/{id}

Response: Full session completion with exercise logs


Exercise Logs

List Exercise Logs for Session

http
GET /v1/patient-session-completions/{id}/exercise-logs

Response:

json
{
  "exercise_logs": [
    {
      "id": 100,
      "exercise_id": "770e8400-...",
      "exercise": {
        "name": "Straight Leg Raise",
        "video_thumbnail_url": "https://..."
      },
      "sort_order": 0,
      "started_at": "2026-02-16T09:02:00Z",
      "completed_at": "2026-02-16T09:05:30Z",
      "prescribed_mode": "reps",
      "prescribed_sets": 3,
      "prescribed_reps": 10,
      "actual_sets": 3,
      "actual_reps": 10,
      "video_watched": true,
      "video_watch_seconds": 38,
      "video_watch_percentage": 90.48,
      "pose_data_captured": true,
      "pose_accuracy_score": 85.50,
      "skipped": false
    }
  ]
}

Progress & Analytics

Get Plan Progress

http
GET /v1/patient-treatment-plans/{id}/progress

Response:

json
{
  "patient_treatment_plan_id": 42,
  "sessions_total": 12,
  "sessions_completed": 6,
  "sessions_skipped": 1,
  "sessions_remaining": 5,
  "completion_percentage": 50.0,
  "current_week": 2,
  "total_weeks": 4,
  "start_date": "2026-02-10",
  "end_date": "2026-03-10",
  "days_remaining": 22,
  "streak": {
    "current": 3,
    "best": 5
  },
  "pain_trend": [
    {"session": 1, "before": 7, "after": 5},
    {"session": 2, "before": 6, "after": 4},
    {"session": 3, "before": 6, "after": 4},
    {"session": 4, "before": 5, "after": 3},
    {"session": 5, "before": 5, "after": 3},
    {"session": 6, "before": 4, "after": 3}
  ],
  "average_session_duration_seconds": 1750,
  "average_completion_rate": 95.0,
  "average_video_watch_percentage": 88.5
}

Get Detailed Analytics (Specialist/Admin)

http
GET /v1/patient-treatment-plans/{id}/analytics

Response:

json
{
  "patient_treatment_plan_id": 42,
  "sessions": [
    {
      "session_number": 1,
      "completions": [
        {
          "completed_at": "2026-02-10T09:30:00Z",
          "duration_seconds": 1680,
          "exercises_completed": 4,
          "exercises_skipped": 0,
          "pain_level_before": 7,
          "pain_level_after": 5,
          "perceived_difficulty": 4
        }
      ]
    }
  ],
  "exercise_performance": [
    {
      "exercise_id": "770e8400-...",
      "exercise_name": "Straight Leg Raise",
      "times_performed": 6,
      "times_skipped": 0,
      "average_completion_rate": 100.0,
      "average_video_watch_percentage": 92.3,
      "average_pose_accuracy": 85.5,
      "trend": "improving"
    }
  ],
  "adherence": {
    "weeks": [
      {"week": 1, "expected": 3, "completed": 3, "rate": 100.0},
      {"week": 2, "expected": 3, "completed": 2, "rate": 66.7}
    ]
  }
}