Exercise Library API Endpoints
Exercises
List Exercises
GET /v1/exercises?category_id=5&body_region_id=3&difficulty=beginner&status=published&q=plank&scope=all&sort=-created_at&page=1&limit=25Query Parameters:
q(string) — Full text search on name/descriptioncategory_id(int) — Filter by categorybody_region_id(int) — Filter by body regionequipment_id(int) — Filter by required equipmentdifficulty(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:
{
"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
GET /v1/exercises/{id}Response:
{
"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)
POST /v1/exercisesRequest:
{
"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': setsorganization_id = current_app_org_id() - If
current_app_role() = 'superadmin'and no org context: setsorganization_id = NULL(global) - Creates
exercise_tagsentries for provided taxonomy IDs
Response: Exercise object (201 Created)
Update Exercise (Admin / Superadmin)
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)
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 RESTRICTFK ontreatment_plan_session_exercisesprevents actual deletion
Response: 204 No Content
Transition Exercise Status
PUT /v1/exercises/{id}/statusRequest:
{
"status": "published"
}Allowed transitions:
draft→publishedpublished→archivedarchived→published(re-publish)archived→draft(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
POST /v1/exercises/{id}/cloneRequest:
{
"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
POST /v1/exercises/{id}/video
Content-Type: multipart/form-dataRequest:
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:
{
"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
DELETE /v1/exercises/{id}/videoBackend:
- Deletes video from CDN
- Nullifies
video_url,video_provider,video_thumbnail_url,video_duration_seconds
Response: 204 No Content
Exercise Instructions
List Instructions
GET /v1/exercises/{id}/instructionsResponse:
{
"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
POST /v1/exercises/{id}/instructionsRequest:
{
"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
PUT /v1/exercises/{id}/instructions/{instructionId}Request: Same as add (partial updates allowed)
Response: Updated instruction object
Delete Instruction
DELETE /v1/exercises/{id}/instructions/{instructionId}Response: 204 No Content
Upload Instruction Image
POST /v1/exercises/{id}/instructions/{instructionId}/image
Content-Type: multipart/form-dataRequest:
file: [binary]Response:
{
"image_url": "https://cdn.example.com/exercises/550e8400.../instructions/1/image.jpg"
}Reorder Instructions
PUT /v1/exercises/{id}/instructions/reorderRequest:
{
"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
GET /v1/exercise-categories?scope=allQuery Parameters:
scope(string) —global,org,all(default)parent_id(int) — Filter children of a specific parent
Response:
{
"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)
POST /v1/exercise-categoriesRequest:
{
"name": "Aquatic Therapy",
"slug": "aquatic-therapy",
"description": "Water-based rehabilitation exercises",
"parent_id": null,
"sort_order": 5
}Response: Category object (201 Created)
Update Category
PUT /v1/exercise-categories/{id}Response: Updated category object
Delete Category
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
GET /v1/exercise-body-regions?body_area=upper_body&scope=allQuery Parameters:
scope(string) —global,org,all(default)body_area(string) — Filter by area (upper_body, lower_body, core, full_body)
Response:
{
"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
GET /v1/exercise-equipment?scope=allResponse:
{
"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.