MSK Assessment Integration Guide
This guide covers integrating the GOFA MSK (Musculoskeletal) Assessment module within your Flutter app using the GOFA WebView SDK.
Overview
The MSK Assessment module provides comprehensive musculoskeletal health evaluations including:
- Assessment Survey: Initial questionnaire about symptoms and history
- Physical Assessment: Guided physical tests and measurements
- Pain Review: Detailed pain location and intensity mapping
- Report Generation: Comprehensive assessment results and recommendations
Event-Driven Integration
The MSK assessment communicates with your Flutter app through events posted via the WebView SDK's updateCallback function. These events allow you to:
- Track assessment progress
- Handle user interactions
- Access generated reports
- Respond to early exits or completion
Key changes (Aug 2025)
- No
statefield is sent. Completion is inferred by the presence of specific data (e.g., whenrisk.*exists, the assessment is considered ended). - Every event includes BOTH of the following when available:
mskAssessResult: the original nested JSON shape as stored by the app (primary/main result).mskAssessResultFlattened: a flattened snapshot of the assessment result (dot-notation keys,assessmentData.prefix removed,createdAtconverted tocreatedAt._seconds/_nanosecondswhen applicable).
- The older, separate nested payloads (
surveyResult/physicalResult/painReviewResult) are deprecated and are not sent.
Your Flutter app receives events via the WebView SDK as an object with { type, data }. The type matches the event names below and data contains the payload.
Event Flow
Implementation
Basic Event Handler
void _handleMSKEvents(String event, dynamic data) {
switch (event) {
case 'START_MSK_ASSESSMENT':
_onAssessmentStarted(data);
break;
case 'FINISH_ASSESSMENT_SURVEY':
_onSurveyCompleted(data);
break;
case 'FINISH_PHYSICAL_ASSESSMENT':
_onPhysicalCompleted(data);
break;
case 'FINISH_PAIN_REVIEW':
_onPainReviewCompleted(data);
break;
case 'GENERATED_REPORT':
// data.reportUrl is a base64 data URL (data:application/pdf;base64,...)
_onReportGenerated(data);
break;
case 'EARLY_QUIT_ASSESSMENT':
_onEarlyQuit(data);
break;
case 'QUIT_MSK_MODULE':
_onModuleQuit();
break;
}
}
From each handler, you can read either data['mskAssessResult'] (nested/original) or data['mskAssessResultFlattened'] (flattened). Choose the one that best fits your storage/analytics needs.
Working with nested vs flattened results
mskAssessResult(nested/original - primary)- Matches the domain model stored by the app; includes
assessmentDatasubtree and optionalrisk.
- Matches the domain model stored by the app; includes
mskAssessResultFlattened(flattened)- Dot-notation keys for nested fields.
assessmentData.prefix removed (e.g.,assessmentData.assessmentSurvey.age->assessmentSurvey.age).createdAtappears ascreatedAt._secondsandcreatedAt._nanosecondswhen applicable.
Example keys (flattened):
{
"assessmentSurvey.age": 45,
"assessmentResults.rangeOfMotion": 85.5,
"painReview.abductionMaxPain.right": "severe",
"risk.SIS.riskLevel": "MODERATE"
}
When risk.* exists in either the nested or flattened payload, the assessment is considered ended.
The older per-phase payloads surveyResult, physicalResult, and painReviewResult are no longer sent. Use mskAssessResult or mskAssessResultFlattened instead.
Assessment Data Reference
The MSK module supports three assessment codes, each with its own survey questions, physical tests, pain review, and risk calculations:
| Assessment Code | Description | Risk Types |
|---|---|---|
shoulder | Shoulder assessment | SIS (Shoulder Impingement Syndrome), FS (Frozen Shoulder) |
low-back | Low back assessment | LB (Low Back Muscle Strain), LDC (Lumbar Degenerative Compression), LDH (Lumbar Disc Herniation) |
knee | Knee assessment | OA (Knee Osteoarthritis), PFPS (Patellofemoral Pain Syndrome) |
Common Base Fields
All assessment results share these base fields:
| Key | Type | Description |
|---|---|---|
id | string | Unique assessment result ID |
clientId | string | Client/tenant identifier |
userId | string (optional) | User identifier |
assessmentCode | "shoulder" | "low-back" | "knee" | Assessment type |
createdAt | ISO string (nested) or ._seconds/._nanoseconds (flattened) | Creation timestamp |
assessmentData | object | Contains assessmentSurvey, assessmentResults, painReview |
risk | object | Risk scores per condition (present when assessment is complete) |
Shoulder Assessment (shoulder)
Nested (mskAssessResult) Example — Shoulder
{
"id": "523bOJ5ky183Qr5qZ61B",
"clientId": "bupa-uat",
"userId": "mzkQP97YwpdjpmWS7a9qZTB4w9t1",
"assessmentCode": "shoulder",
"createdAt": "2025-08-07T12:32:58.031Z",
"assessmentData": {
"assessmentSurvey": {
"version": "SHOULDER_ASSESSMENT_SURVEY_CONTENT_V1",
"shoulderPainRecurring": false,
"shoulderPainRelievedByRest": false,
"shoulderPainOverheadActivity": true,
"gender": "male",
"shoulderPainInModerateMotion": false,
"shoulderPainFixedPosition": false,
"shoulderPainWithHandBehindBack": false,
"shoulderPainRepetitiveMotion": false,
"shoulderPainWithOverheadActivity": true,
"shoulderPainAtLimit": true,
"shoulderPainNoInjury": true,
"diabetes": "none",
"age": 45,
"shoulderRangeOfMotionLimited": false
},
"assessmentResults": {
"version": "SHOULDER_ASSESSMENT_RESULTS_V1",
"rangeOfMotion": 85.5,
"strengthScore": 78.2,
"abduction": { "left": 180, "right": 180 },
"flexion": { "left": 160, "right": 160 },
"extension": { "left": 60, "right": 60 },
"externalRotation": { "left": 90, "right": 90 },
"internalRotation": { "left": 70, "right": 70 },
"compression": { "left": 45, "right": 45 }
},
"painReview": {
"version": "SHOULDER_PAIN_REVIEW_CONTENT_V1",
"flexionMidPain": { "left": "moderate", "right": "moderate" },
"flexionMaxPain": { "left": "moderate", "right": "moderate" },
"abductionMidPain": { "left": "moderate", "right": "moderate" },
"abductionMaxPain": { "left": "mild", "right": "severe" },
"rotationMidPain": { "left": "moderate", "right": "moderate" },
"rotationMaxPain": { "left": "moderate", "right": "severe" },
"pressElbowPain": { "left": "moderate", "right": "moderate" }
}
},
"risk": {
"SIS": { "percent": 28.999999999999996, "riskLevel": "MODERATE" },
"FS": { "percent": 24, "riskLevel": "LOW" }
}
}
Flattened (mskAssessResultFlattened) Example — Shoulder
{
"id": "523bOJ5ky183Qr5qZ61B",
"clientId": "bupa-uat",
"userId": "mzkQP97YwpdjpmWS7a9qZTB4w9t1",
"assessmentCode": "shoulder",
"createdAt._seconds": 1751949178,
"createdAt._nanoseconds": 31000000,
"assessmentSurvey.version": "SHOULDER_ASSESSMENT_SURVEY_CONTENT_V1",
"assessmentSurvey.shoulderPainRecurring": false,
"assessmentSurvey.gender": "male",
"assessmentSurvey.age": 45,
"assessmentResults.abduction.left": 180,
"assessmentResults.abduction.right": 180,
"painReview.abductionMaxPain.left": "mild",
"painReview.abductionMaxPain.right": "severe",
"risk.SIS.percent": 29,
"risk.SIS.riskLevel": "MODERATE",
"risk.FS.percent": 24,
"risk.FS.riskLevel": "LOW"
}
The flattened format uses dot-notation keys and removes the assessmentData. prefix. The example above is abbreviated — actual payloads include all fields.
Field Reference — Shoulder
Assessment Survey fields (assessmentSurvey.):
| Key | Type | Description |
|---|---|---|
version | "SHOULDER_ASSESSMENT_SURVEY_CONTENT_V1" | Schema version |
age | number | User age (18–100) |
gender | "male" | "female" | "undefined" | User gender |
diabetes | "none" | "type1" | "type2" | Diabetes status |
shoulderPainRecurring | boolean | Recurring shoulder pain |
shoulderPainRelievedByRest | boolean | Pain relieved by rest |
shoulderPainOverheadActivity | boolean | Pain during overhead activity |
shoulderPainInModerateMotion | boolean | Pain in moderate motion |
shoulderPainFixedPosition | boolean | Pain in fixed position |
shoulderPainWithHandBehindBack | boolean | Pain with hand behind back |
shoulderPainRepetitiveMotion | boolean | Pain with repetitive motion |
shoulderPainWithOverheadActivity | boolean | Pain with overhead activity |
shoulderPainAtLimit | boolean | Pain at limit of motion |
shoulderPainNoInjury | boolean | Pain without injury |
shoulderRangeOfMotionLimited | boolean | Limited range of motion |
Assessment Results fields (assessmentResults.):
| Key | Type | Description |
|---|---|---|
version | "SHOULDER_ASSESSMENT_RESULTS_V1" | Schema version |
abduction.left, abduction.right | number | Abduction angle (degrees) |
flexion.left, flexion.right | number | Flexion angle (degrees) |
extension.left, extension.right | number | Extension angle (degrees) |
externalRotation.left, externalRotation.right | number | External rotation angle |
internalRotation.left, internalRotation.right | number | Internal rotation angle |
compression.left, compression.right | number | Compression test result |
Pain Review fields (painReview.):
| Key | Type | Description |
|---|---|---|
version | "SHOULDER_PAIN_REVIEW_CONTENT_V1" | Schema version |
flexionMidPain.left, flexionMidPain.right | "none" | "mild" | "moderate" | "severe" | Mid-flexion pain |
flexionMaxPain.left, flexionMaxPain.right | "none" | "mild" | "moderate" | "severe" | Max flexion pain |
abductionMidPain.left, abductionMidPain.right | "none" | "mild" | "moderate" | "severe" | Mid-abduction pain |
abductionMaxPain.left, abductionMaxPain.right | "none" | "mild" | "moderate" | "severe" | Max abduction pain |
rotationMidPain.left, rotationMidPain.right | "none" | "mild" | "moderate" | "severe" | Mid-rotation pain |
rotationMaxPain.left, rotationMaxPain.right | "none" | "mild" | "moderate" | "severe" | Max rotation pain |
pressElbowPain.left, pressElbowPain.right | "none" | "mild" | "moderate" | "severe" | Elbow press pain |
Risk Assessment fields (risk.):
| Key | Description |
|---|---|
SIS.percent, SIS.riskLevel | Shoulder Impingement Syndrome risk |
FS.percent, FS.riskLevel | Frozen Shoulder risk |
Low-Back Assessment (low-back)
Nested (mskAssessResult) Example — Low-Back
{
"id": "abc123LowBackId",
"clientId": "bupa-uat",
"userId": "userXYZ123",
"assessmentCode": "low-back",
"createdAt": "2025-08-10T09:15:00.000Z",
"assessmentData": {
"assessmentSurvey": {
"version": "LOW_BACK_ASSESSMENT_SURVEY_CONTENT_V1",
"gender": "male",
"isPainFromSportsOrHandling": true,
"isPainWorseSittingOrStanding": true,
"hasSwellingOrWarmth": false,
"hasParaspinalMusclePain": true,
"hasRopeLikeLump": false,
"hasMorningStiffness": true,
"isPainWithProlongedSitting": true,
"isPainWithBackExtension": false,
"isPainWithProlongedWalking": true,
"isPainWithForwardBending": true,
"isPainWithKneesToChest": false,
"isPainWithCoughingSneezing": false,
"isPainRelievedByProneLying": true
},
"assessmentResults": {
"version": "LOW_BACK_ASSESSMENT_RESULTS_V1",
"seatedForwardFlexion": {
"normalizedFingerToToeDistance": -0.15
}
},
"painReview": {
"version": "LOW_BACK_PAIN_REVIEW_CONTENT_V1",
"painLevel": { "left": "moderate", "right": "mild" },
"painTiming": "midrange",
"radiatingPainTiming": "none"
}
},
"risk": {
"LB": { "percent": 45, "riskLevel": "MODERATE" },
"LDC": { "percent": 30, "riskLevel": "LOW" },
"LDH": { "percent": 25, "riskLevel": "LOW" }
}
}
Field Reference — Low-Back
Assessment Survey fields (assessmentSurvey.):
| Key | Type | Description |
|---|---|---|
version | "LOW_BACK_ASSESSMENT_SURVEY_CONTENT_V1" | Schema version |
gender | "male" | "female" | "undefined" | User gender |
isPainFromSportsOrHandling | boolean | Pain from sports or heavy lifting |
isPainWorseSittingOrStanding | boolean | Pain worsens when sitting/standing |
hasSwellingOrWarmth | boolean | Swelling or warmth present |
hasParaspinalMusclePain | boolean | Paraspinal muscle pain |
hasRopeLikeLump | boolean | Rope-like lump present |
hasMorningStiffness | boolean | Morning stiffness |
isPainWithProlongedSitting | boolean | Pain with prolonged sitting |
isPainWithBackExtension | boolean | Pain with back extension |
isPainWithProlongedWalking | boolean | Pain with prolonged walking |
isPainWithForwardBending | boolean | Pain with forward bending |
isPainWithKneesToChest | boolean | Pain with knees to chest |
isPainWithCoughingSneezing | boolean | Pain with coughing/sneezing |
isPainRelievedByProneLying | boolean | Pain relieved by prone lying |
Assessment Results fields (assessmentResults.):
| Key | Type | Description |
|---|---|---|
version | "LOW_BACK_ASSESSMENT_RESULTS_V1" | Schema version |
seatedForwardFlexion.normalizedFingerToToeDistance | number | Finger-to-toe distance normalized by leg length. 0 = touching toes, negative = not reaching, positive = beyond toes |
Pain Review fields (painReview.):
| Key | Type | Description |
|---|---|---|
version | "LOW_BACK_PAIN_REVIEW_CONTENT_V1" | Schema version |
painLevel.left | "none" | "mild" | "moderate" | "severe" | Left back pain level |
painLevel.right | "none" | "mild" | "moderate" | "severe" | Right back pain level |
painTiming | "none" | "midrange" | "endpoint" | "constant" | When pain occurs during test |
radiatingPainTiming | "none" | "early" | "midrange" | "endpoint" | When radiating pain occurs |
Risk Assessment fields (risk.):
| Key | Description |
|---|---|
LB.percent, LB.riskLevel | Low Back Muscle Strain risk |
LDC.percent, LDC.riskLevel | Lumbar Degenerative Compression risk |
LDH.percent, LDH.riskLevel | Lumbar Disc Herniation risk |
Knee Assessment (knee)
Nested (mskAssessResult) Example — Knee
{
"id": "knee456ExampleId",
"clientId": "bupa-uat",
"userId": "userABC789",
"assessmentCode": "knee",
"createdAt": "2025-08-12T14:30:00.000Z",
"assessmentData": {
"assessmentSurvey": {
"version": "KNEE_ASSESSMENT_SURVEY_CONTENT_V1",
"ageOver35": true,
"gender": "female",
"isOverweight": false,
"hasKneeInjuryOrSurgery": false,
"hasMorningKneeStiffnessOrPain": true,
"hasProlongedSittingDiscomfort": true,
"hasKneePainOnWalkingStairsOrSquat": true,
"hasAnteriorKneePainOnFlexion": false,
"hasKneePainOnStandingUp": true,
"hasKneeLockingOrInstability": false,
"hasKneePainLimitingDailyTasks": false
},
"assessmentResults": {
"version": "KNEE_ASSESSMENT_RESULTS_V1",
"squat": {
"angle": 95,
"valgus": { "left": 5, "right": 8 }
}
},
"painReview": {
"version": "KNEE_PAIN_REVIEW_CONTENT_V1",
"painLevel": { "left": "mild", "right": "moderate" },
"painTiming": "endpoint",
"hasMildDiscomfortNearFullDepth": true,
"hasSharpAnteriorKneePainMidRange": false
}
},
"risk": {
"OA": { "percent": 35, "riskLevel": "MODERATE" },
"PFPS": { "percent": 20, "riskLevel": "LOW" }
}
}
Field Reference — Knee
Assessment Survey fields (assessmentSurvey.):
| Key | Type | Description |
|---|---|---|
version | "KNEE_ASSESSMENT_SURVEY_CONTENT_V1" | Schema version |
ageOver35 | boolean (optional) | User is over 35 years old |
gender | "male" | "female" | "undefined" | User gender |
isOverweight | boolean | BMI ≥ 25 |
hasKneeInjuryOrSurgery | boolean | Previous knee injury or surgery |
hasMorningKneeStiffnessOrPain | boolean | Morning stiffness or pain |
hasProlongedSittingDiscomfort | boolean | Discomfort after prolonged sitting |
hasKneePainOnWalkingStairsOrSquat | boolean | Pain when walking stairs or squatting |
hasAnteriorKneePainOnFlexion | boolean | Anterior knee pain on flexion |
hasKneePainOnStandingUp | boolean | Pain when standing up |
hasKneeLockingOrInstability | boolean | Knee locking or instability |
hasKneePainLimitingDailyTasks | boolean | Pain limits daily activities |
Assessment Results fields (assessmentResults.):
| Key | Type | Description |
|---|---|---|
version | "KNEE_ASSESSMENT_RESULTS_V1" | Schema version |
squat.angle | number (0–180) | Squat depth angle in degrees |
squat.valgus.left | number | Left knee valgus (inward bending) measurement |
squat.valgus.right | number | Right knee valgus (inward bending) measurement |
Pain Review fields (painReview.):
| Key | Type | Description |
|---|---|---|
version | "KNEE_PAIN_REVIEW_CONTENT_V1" | Schema version |
painLevel.left | "none" | "mild" | "moderate" | "severe" | Left knee pain level |
painLevel.right | "none" | "mild" | "moderate" | "severe" | Right knee pain level |
painTiming | "none" | "midrange" | "endpoint" | "constant" | When pain occurs during squat |
hasMildDiscomfortNearFullDepth | boolean | Mild discomfort only at full squat depth |
hasSharpAnteriorKneePainMidRange | boolean | Sharp anterior knee pain at mid-range |
Risk Assessment fields (risk.):
| Key | Description |
|---|---|
OA.percent, OA.riskLevel | Knee Osteoarthritis risk |
PFPS.percent, PFPS.riskLevel | Patellofemoral Pain Syndrome risk |
Event Details
START_MSK_ASSESSMENT
When: User initiates a new MSK assessment Use Cases: Track assessment start, initialize local state, show progress indicator
// Data structure:
{
'assessmentResultId': 'uuid-string',
'assessmentCode': 'string',
'createdAt': 'iso-date-string',
'userId': 'string-or-null',
'mskAssessResult': { /* nested/original result (may be partial early on) */ },
'mskAssessResultFlattened': { /* flattened snapshot (may be partial) */ }
}
FINISH_ASSESSMENT_SURVEY
When: User completes the initial questionnaire Use Cases: Update progress, save intermediate state, track completion rate
// Data structure:
{
'assessmentResultId': 'uuid-string',
'mskAssessResult': { /* nested/original */ },
'mskAssessResultFlattened': { /* flattened */ }
}
FINISH_PHYSICAL_ASSESSMENT
When: User completes all physical tests Use Cases: Update progress, prepare for pain review phase
// Data structure:
{
'assessmentResultId': 'uuid-string',
'mskAssessResult': { /* nested/original */ },
'mskAssessResultFlattened': { /* flattened */ }
}
FINISH_PAIN_REVIEW
When: User completes pain location and intensity mapping Use Cases: Update progress, prepare for report generation
// Data structure:
{
'assessmentResultId': 'uuid-string',
'mskAssessResult': { /* nested/original */ },
'mskAssessResultFlattened': { /* flattened */ }
}
GENERATED_REPORT
When: System completes assessment processing and generates final report Use Cases: Notify user, save report data, display or download PDF, show completion
// Data structure:
{
'assessmentResultId': 'uuid-string',
'reportUrl': 'data:application/pdf;base64,...', // Base64 data URL of the PDF
'mskAssessResult': { /* nested/original */ },
'mskAssessResultFlattened': { /* flattened */ }
}
Handling the PDF Data URL
The reportUrl is provided as a base64 data URL that you can use directly in your Flutter app:
void _onReportGenerated(Map<String, dynamic> data) {
final String reportUrl = data['reportUrl']; // data:application/pdf;base64,...
// Extract base64 data
final String base64Data = reportUrl.split(',')[1];
final Uint8List pdfBytes = base64Decode(base64Data);
// Save to device storage
_savePdfToFile(pdfBytes, 'msk_report_${data['assessmentResultId']}.pdf');
// Or display in a PDF viewer
_showPdfViewer(pdfBytes);
}
Future<void> _savePdfToFile(Uint8List pdfBytes, String filename) async {
final directory = await getApplicationDocumentsDirectory();
final file = File('${directory.path}/$filename');
await file.writeAsBytes(pdfBytes);
print('PDF saved to: ${file.path}');
}
EARLY_QUIT_ASSESSMENT
When: User exits assessment before completion Use Cases: Handle incomplete state, show retention messaging, track dropout reasons
// Data structure:
{
'assessmentResultId': 'uuid-string-or-null',
'reason': 'string-or-null',
'mskAssessResult': { /* optional partial */ },
'mskAssessResultFlattened': { /* optional partial */ }
}
QUIT_MSK_MODULE
When: User exits the entire MSK module Use Cases: Navigate back, clear state, track module engagement
// Data structure:
{
'mskAssessResult': { /* optional */ },
'mskAssessResultFlattened': { /* optional */ }
}