Skip to content

Exercise Library API Endpoints

Exercises

List Exercises

http
GET /v1/exercises?category_id=5&body_region_id=3&difficulty=beginner&status=published&q=plank&scope=all&sort=-created_at&page=1&limit=25

Query Parameters:

  • q (string) — Full text search on name/description
  • category_id (int) — Filter by category
  • body_region_id (int) — Filter by body region
  • equipment_id (int) — Filter by required equipment
  • difficulty (string) — Filter by difficulty (beginner, intermediate, advanced)
  • status (string) — Filter by status (draft, published, archived)
  • scope (string) — global (platform only), org (org only), all (default, both)
  • has_video (boolean) — Filter exercises with/without video
  • Standard pagination: page, limit, sort

Response:

json
{
  "exercises": [
    {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "organization_id": null,
      "name": "Shoulder External Rotation",
      "slug": "shoulder-external-rotation",
      "description": "Strengthening exercise for the rotator cuff...",
      "difficulty": "beginner",
      "estimated_duration_seconds": 45,
      "video_url": "https://cdn.example.com/exercises/shoulder-ext-rot.m3u8",
      "video_provider": "bunny_stream",
      "video_thumbnail_url": "https://cdn.example.com/exercises/shoulder-ext-rot-thumb.jpg",
      "video_duration_seconds": 42,
      "status": "published",
      "cloned_from_id": null,
      "categories": [
        {"id": 2, "name": "Strengthening", "slug": "strengthening"}
      ],
      "body_regions": [
        {"id": 1, "name": "Shoulder", "slug": "shoulder", "body_area": "upper_body"}
      ],
      "equipment": [
        {"id": 3, "name": "Resistance Band", "slug": "resistance-band"}
      ],
      "contraindications_count": 1,
      "instructions_count": 4,
      "created_at": "2026-01-15T10:00:00Z"
    }
  ],
  "total": 156,
  "page": 1,
  "limit": 25,
  "total_pages": 7
}

Get Exercise Details

http
GET /v1/exercises/{id}

Response:

json
{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "organization_id": null,
  "name": "Shoulder External Rotation",
  "slug": "shoulder-external-rotation",
  "description": "Strengthening exercise for the rotator cuff muscles...",
  "instructions_summary": "Lie on side, rotate forearm upward keeping elbow pinned",
  "difficulty": "beginner",
  "estimated_duration_seconds": 45,
  "video_url": "https://cdn.example.com/exercises/shoulder-ext-rot.m3u8",
  "video_provider": "bunny_stream",
  "video_thumbnail_url": "https://cdn.example.com/exercises/shoulder-ext-rot-thumb.jpg",
  "video_duration_seconds": 42,
  "status": "published",
  "cloned_from_id": null,
  "created_by_user_id": 1,
  "categories": [
    {"id": 2, "name": "Strengthening", "slug": "strengthening"}
  ],
  "body_regions": [
    {"id": 1, "name": "Shoulder", "slug": "shoulder", "body_area": "upper_body"}
  ],
  "equipment": [
    {"id": 3, "name": "Resistance Band", "slug": "resistance-band"}
  ],
  "instructions": [
    {
      "id": 1,
      "sort_order": 0,
      "title": "Starting Position",
      "content": "Lie on your side with a towel roll under your arm...",
      "image_url": "https://...",
      "instruction_type": "preparation"
    },
    {
      "id": 2,
      "sort_order": 1,
      "title": "Movement",
      "content": "Rotate your forearm upward, keeping elbow at 90 degrees...",
      "image_url": null,
      "instruction_type": "step"
    },
    {
      "id": 3,
      "sort_order": 2,
      "title": null,
      "content": "Keep your elbow pinned to your side throughout the movement",
      "image_url": null,
      "instruction_type": "form_cue"
    },
    {
      "id": 4,
      "sort_order": 3,
      "title": null,
      "content": "Stop if you feel sharp pain in the shoulder joint",
      "image_url": null,
      "instruction_type": "safety"
    }
  ],
  "contraindications": [
    {
      "id": 1,
      "condition_name": "Acute Shoulder Dislocation",
      "description": "Do not perform within 6 weeks of shoulder dislocation",
      "severity": "contraindicated"
    }
  ],
  "created_at": "2026-01-15T10:00:00Z",
  "updated_at": "2026-02-10T14:30:00Z"
}

Create Exercise (Admin / Superadmin)

http
POST /v1/exercises

Request:

json
{
  "name": "Shoulder External Rotation",
  "slug": "shoulder-external-rotation",
  "description": "Strengthening exercise for the rotator cuff muscles",
  "instructions_summary": "Lie on side, rotate forearm upward keeping elbow pinned",
  "difficulty": "beginner",
  "estimated_duration_seconds": 45,
  "status": "draft",
  "category_ids": [2],
  "body_region_ids": [1],
  "equipment_ids": [3]
}

Backend:

  • If current_app_role() = 'admin': sets organization_id = current_app_org_id()
  • If current_app_role() = 'superadmin' and no org context: sets organization_id = NULL (global)
  • Creates exercise_tags entries for provided taxonomy IDs

Response: Exercise object (201 Created)

Update Exercise (Admin / Superadmin)

http
PUT /v1/exercises/{id}

Request: Same as create (partial updates allowed)

Validation:

  • Admin can only update exercises where organization_id = current_app_org_id()
  • Superadmin can update any exercise (including global)

Response: Updated exercise object

Soft Delete Exercise (Admin / Superadmin)

http
DELETE /v1/exercises/{id}

Backend:

  • Sets deleted_at = NOW() (soft delete)
  • If exercise is referenced in active treatment plan sessions: returns 409 Conflict with message indicating the exercise is in use
  • The ON DELETE RESTRICT FK on treatment_plan_session_exercises prevents actual deletion

Response: 204 No Content

Transition Exercise Status

http
PUT /v1/exercises/{id}/status

Request:

json
{
  "status": "published"
}

Allowed transitions:

  • draftpublished
  • publishedarchived
  • archivedpublished (re-publish)
  • archiveddraft (back to editing)

Validation:

  • Publishing requires: name, at least one instruction step
  • Video is recommended but not required for publishing

Response: Updated exercise object

Clone Exercise

http
POST /v1/exercises/{id}/clone

Request:

json
{
  "name": "Custom Shoulder Rotation (Modified)",
  "slug": "custom-shoulder-rotation-modified"
}

Backend:

  • Copies all exercise fields (except id, slug, status, organization_id)
  • Copies all instructions (with images)
  • Copies all contraindications
  • Copies all tags
  • Sets organization_id = current_app_org_id()
  • Sets cloned_from_id = source exercise ID
  • Sets status = 'draft'
  • Uses provided name/slug or auto-generates from source

Response: New exercise object (201 Created)


Exercise Video

Upload Video

http
POST /v1/exercises/{id}/video
Content-Type: multipart/form-data

Request:

file: [binary]

Backend:

  • Uploads to CDN (Bunny Stream or S3)
  • For Bunny Stream: triggers automatic HLS transcoding
  • Updates exercises.video_url, video_provider, video_duration_seconds
  • Auto-generates thumbnail if CDN supports it, updates video_thumbnail_url

Response:

json
{
  "video_url": "https://cdn.example.com/exercises/550e8400.../video.m3u8",
  "video_provider": "bunny_stream",
  "video_thumbnail_url": "https://cdn.example.com/exercises/550e8400.../thumb.jpg",
  "video_duration_seconds": 42
}

Remove Video

http
DELETE /v1/exercises/{id}/video

Backend:

  • Deletes video from CDN
  • Nullifies video_url, video_provider, video_thumbnail_url, video_duration_seconds

Response: 204 No Content


Exercise Instructions

List Instructions

http
GET /v1/exercises/{id}/instructions

Response:

json
{
  "instructions": [
    {
      "id": 1,
      "sort_order": 0,
      "title": "Starting Position",
      "content": "Lie on your side...",
      "image_url": null,
      "instruction_type": "preparation",
      "created_at": "2026-01-15T10:00:00Z"
    }
  ]
}

Add Instruction Step

http
POST /v1/exercises/{id}/instructions

Request:

json
{
  "sort_order": 1,
  "title": "Movement",
  "content": "Rotate your forearm upward, keeping elbow at 90 degrees",
  "instruction_type": "step"
}

Response: Instruction object (201 Created)

Update Instruction

http
PUT /v1/exercises/{id}/instructions/{instructionId}

Request: Same as add (partial updates allowed)

Response: Updated instruction object

Delete Instruction

http
DELETE /v1/exercises/{id}/instructions/{instructionId}

Response: 204 No Content

Upload Instruction Image

http
POST /v1/exercises/{id}/instructions/{instructionId}/image
Content-Type: multipart/form-data

Request:

file: [binary]

Response:

json
{
  "image_url": "https://cdn.example.com/exercises/550e8400.../instructions/1/image.jpg"
}

Reorder Instructions

http
PUT /v1/exercises/{id}/instructions/reorder

Request:

json
{
  "instruction_ids": [3, 1, 2, 4]
}

Backend: Updates sort_order for each instruction based on array position.

Response: Updated instructions list


Exercise Categories

List Categories

http
GET /v1/exercise-categories?scope=all

Query Parameters:

  • scope (string) — global, org, all (default)
  • parent_id (int) — Filter children of a specific parent

Response:

json
{
  "categories": [
    {
      "id": 1,
      "organization_id": null,
      "name": "Stretching",
      "slug": "stretching",
      "description": "Flexibility and range-of-motion exercises",
      "parent_id": null,
      "sort_order": 0,
      "children": [
        {"id": 4, "name": "Static Stretching", "slug": "static-stretching", "parent_id": 1},
        {"id": 5, "name": "Dynamic Stretching", "slug": "dynamic-stretching", "parent_id": 1}
      ]
    },
    {
      "id": 2,
      "organization_id": null,
      "name": "Strengthening",
      "slug": "strengthening",
      "description": "Muscle strengthening exercises",
      "parent_id": null,
      "sort_order": 1,
      "children": []
    }
  ]
}

Create Category (Admin / Superadmin)

http
POST /v1/exercise-categories

Request:

json
{
  "name": "Aquatic Therapy",
  "slug": "aquatic-therapy",
  "description": "Water-based rehabilitation exercises",
  "parent_id": null,
  "sort_order": 5
}

Response: Category object (201 Created)

Update Category

http
PUT /v1/exercise-categories/{id}

Response: Updated category object

Delete Category

http
DELETE /v1/exercise-categories/{id}

Backend:

  • Children get parent_id = NULL (become top-level)
  • Exercise tags referencing this category are removed

Response: 204 No Content


Exercise Body Regions

List Body Regions

http
GET /v1/exercise-body-regions?body_area=upper_body&scope=all

Query Parameters:

  • scope (string) — global, org, all (default)
  • body_area (string) — Filter by area (upper_body, lower_body, core, full_body)

Response:

json
{
  "body_regions": [
    {
      "id": 1,
      "organization_id": null,
      "name": "Shoulder",
      "slug": "shoulder",
      "body_area": "upper_body",
      "sort_order": 0
    }
  ]
}

Create / Update / Delete

Same CRUD pattern as categories.


Exercise Equipment

List Equipment

http
GET /v1/exercise-equipment?scope=all

Response:

json
{
  "equipment": [
    {
      "id": 1,
      "organization_id": null,
      "name": "No Equipment",
      "slug": "no-equipment",
      "icon_url": null,
      "sort_order": 0
    },
    {
      "id": 2,
      "organization_id": null,
      "name": "Resistance Band",
      "slug": "resistance-band",
      "icon_url": "https://...",
      "sort_order": 1
    }
  ]
}

Create / Update / Delete

Same CRUD pattern as categories.