Implementation Guide
A complete reference for integrating the FHIR Risk Assessment Engine into your Medicare Advantage workflow
Quick Navigation
This guide walks you through the engine end-to-end:
Understand the Engine
Learn what the risk assessment engine does and why it matters for Medicare Advantage compliance.
Go to Section 1Send Data to the Engine
Learn the accepted FHIR resource formats, required fields, and how to structure your API requests.
Go to Section 2Interpret the Output
Understand every field in the API response, what each risk level means, and how to act on the results.
Go to Section 3End-to-End Walkthrough
Follow a complete example from building a FHIR Bundle to reading the assessment results.
Go to Section 4[1] Engine Purpose
What This Engine Does
The FHIR Risk Assessment Engine evaluates diagnosis codes submitted by Medicare Advantage organizations to identify codes that are likely to be flagged in CMS or OIG audits. It cross-references each diagnosis against supporting clinical evidence — encounters, medications, and procedures — to determine whether the code is adequately supported.
Why It Matters
CMS recovers billions annually from Medicare Advantage plans through Risk Adjustment Data Validation (RADV) audits. The OIG has identified specific diagnosis categories that are frequently miscoded, leading to improper payments. This engine targets the four highest-risk categories:
Acute Stroke HCC 100
Stroke codes used in outpatient settings without an inpatient stay are a top audit target. Often "history of stroke" is the correct code.
Acute MI HCC 86
Acute heart attack codes without a matching inpatient admission within 60 days suggest potential miscoding as "old MI."
Embolism HCC 107/108
Embolism diagnoses without anticoagulant prescriptions within 90 days often indicate the condition is historical, not active.
Lung Cancer HCC 9
Active lung cancer codes without radiation, chemo, or surgery within 6 months suggest the patient may be in remission.
How It Works — At a Glance
| Step | What Happens |
|---|---|
| 1 | You submit FHIR resources (Conditions, Encounters, Medications, Procedures) via POST /api/assess |
| 2 | The engine validates the FHIR structure and extracts resources by type |
| 3 | Each Condition is checked against CMS/OIG high-risk filters (stroke, MI, embolism, lung cancer) |
| 4 | The engine looks for supporting evidence (encounters, medications, procedures) within defined time windows |
| 5 | Each diagnosis receives a risk level (high, moderate, or low) with a human-readable reason |
[2] Sending Data to the Engine
API Endpoint
POST /api/assess
Content-Type: application/json
Accepted Input Formats
The engine accepts three input structures. Use whichever fits your system best:
A standard FHIR Bundle wrapping all resources in an entry array. This is the most common format used in FHIR integrations.
{
"resourceType": "Bundle",
"type": "collection",
"entry": [
{
"resource": {
"resourceType": "Condition",
"id": "cond-001",
"code": {
"coding": [{
"system": "http://hl7.org/fhir/sid/icd-10-cm",
"code": "I63.9",
"display": "Cerebral infarction, unspecified"
}]
},
"subject": { "reference": "Patient/pat-001" },
"onsetDateTime": "2024-09-15"
}
},
{
"resource": {
"resourceType": "Encounter",
"id": "enc-001",
"class": { "code": "AMB" },
"subject": { "reference": "Patient/pat-001" },
"period": { "start": "2024-09-15" }
}
}
]
}
A simple JSON array containing FHIR resources directly.
[
{
"resourceType": "Condition",
"id": "cond-001",
"code": {
"coding": [{
"system": "http://hl7.org/fhir/sid/icd-10-cm",
"code": "I21.3",
"display": "STEMI of unspecified site"
}]
},
"subject": { "reference": "Patient/12345" },
"onsetDateTime": "2024-09-01"
},
{
"resourceType": "Encounter",
"id": "enc-001",
"class": { "code": "IMP" },
"subject": { "reference": "Patient/12345" },
"period": { "start": "2024-09-01", "end": "2024-09-05" }
}
]
A single FHIR resource (typically a Condition). Useful for quick single-code checks, but without supporting resources the engine cannot perform full validation.
{
"resourceType": "Condition",
"id": "cond-001",
"code": {
"coding": [{
"system": "http://hl7.org/fhir/sid/icd-10-cm",
"code": "C34.1",
"display": "Malignant neoplasm of upper lobe, bronchus or lung"
}]
},
"subject": { "reference": "Patient/67890" },
"onsetDateTime": "2024-06-01"
}
FHIR Resource Reference
Each resource type plays a specific role in the risk assessment. The more supporting resources you include, the more accurate the assessment.
| Resource Type | Required? | Key Fields | Used For |
|---|---|---|---|
| Condition | Required | code.coding[].system, code.coding[].code, onsetDateTime, subject.reference |
The diagnosis code to evaluate |
| Encounter | Optional | class.code, period.start, subject.reference |
Validates inpatient stays for Stroke and MI rules |
| MedicationRequest | Optional | medicationCodeableConcept.coding[].code, authoredOn |
Validates anticoagulant prescriptions for Embolism rule |
| Procedure | Optional | code.coding[].code, performedDateTime or performedPeriod.start |
Validates treatment for Lung Cancer rule |
Supported Code Systems
The engine recognizes the following ICD-10 code system URIs in Condition.code.coding[].system:
http://hl7.org/fhir/sid/icd-10http://hl7.org/fhir/sid/icd-10-cmhttp://www.cms.gov/Medicare/Coding/ICD10
Encounter Class Codes
The engine checks the Encounter.class.code field for inpatient context. Recognized values:
| Code | Meaning | Triggers Validation |
|---|---|---|
IMP | Inpatient | |
ACUTE | Acute Care | |
inpatient | Inpatient (alternate) | |
acute | Acute (alternate) | |
AMB | Ambulatory / Outpatient |
[3] Interpreting the Output
Response Structure
Every successful response follows this structure:
{
"status": "success",
"diagnosis_results": [
{
"code": "I63.9",
"description": "Cerebral infarction, unspecified",
"risk_level": "high",
"reason": "One stroke diagnosis on physician claim, no inpatient/outpatient claim"
},
{
"code": "I21.3",
"description": "STEMI of unspecified site",
"risk_level": "low",
"reason": "No risk factors identified"
}
]
}
Field-by-Field Breakdown
| Field | Type | Description |
|---|---|---|
status |
string | "success" or "error". Always check this first. |
diagnosis_results |
array | One entry per Condition resource submitted. Each contains the assessment. |
diagnosis_results[].code |
string | The ICD-10 code that was evaluated (e.g., "I63.9"). |
diagnosis_results[].description |
string | Human-readable name of the diagnosis. |
diagnosis_results[].risk_level |
string | One of "high", "moderate", or "low". See risk level guide below. |
diagnosis_results[].reason |
string | Explains why the code received this risk level. Actionable for coders and auditors. |
Understanding Risk Levels
Action Required
The diagnosis code matches a CMS/OIG high-risk pattern and lacks required supporting evidence. This code is likely to be flagged in an audit. Review the reason field and gather supporting documentation or correct the code.
Review Recommended
Some risk factors are present but not all high-risk criteria are met. Manual review is recommended to confirm the code is properly supported.
No Action Needed
No risk factors identified. The diagnosis code either does not fall into a high-risk category or has adequate supporting evidence. Likely to pass audit.
What Triggers Each Risk Level
| Condition Category | Flagged as HIGH When | Reason Message |
|---|---|---|
| Acute Stroke (I63.x, G45.x) | No inpatient encounter (class IMP/ACUTE) within ±7 days of onset |
"One stroke diagnosis on physician claim, no inpatient/outpatient claim" |
| Acute MI (I21.x, I22.x) | No inpatient encounter within ±60 days of onset | "No inpatient diagnosis within 60-day window for acute myocardial infarction" |
| Embolism (I26.x, I74.x) | No anticoagulant medication (Warfarin, Apixaban, Rivaroxaban, Dabigatran) within ±90 days | "No matching anticoagulant medication event" |
| Lung Cancer (C34.x) | No radiation, chemotherapy, or surgery procedure within ±6 months (180 days) | "No radiation, chemo, or surgery within ±6 months" |
Error Responses
When something goes wrong, the response includes an error field instead of diagnosis_results:
{
"status": "error",
"error": "No data provided or invalid JSON format"
}
| HTTP Code | Meaning | Common Cause |
|---|---|---|
400 | Bad Request | Missing or malformed JSON, invalid FHIR structure, missing required fields |
500 | Internal Server Error | Unexpected server-side failure — contact support |
[4] End-to-End Walkthrough
Let's walk through a complete example: a patient with an acute stroke diagnosis seen in an outpatient setting.
Step 1 — Build Your FHIR Bundle
Assemble the patient's Condition (the diagnosis) and any supporting Encounter data into a FHIR Bundle:
{
"resourceType": "Bundle",
"type": "collection",
"entry": [
{
"resource": {
"resourceType": "Condition",
"id": "cond-stroke-01",
"code": {
"coding": [{
"system": "http://hl7.org/fhir/sid/icd-10-cm",
"code": "I63.9",
"display": "Cerebral infarction, unspecified"
}]
},
"subject": { "reference": "Patient/pat-100" },
"onsetDateTime": "2024-09-15"
}
},
{
"resource": {
"resourceType": "Encounter",
"id": "enc-outpatient-01",
"class": { "code": "AMB" },
"subject": { "reference": "Patient/pat-100" },
"period": {
"start": "2024-09-15",
"end": "2024-09-15"
}
}
}
]
}
Step 2 — Send the Request
curl -X POST https://your-domain.com/api/assess \
-H "Content-Type: application/json" \
-d @bundle.json
import requests
import json
with open("bundle.json") as f:
bundle = json.load(f)
response = requests.post(
"https://your-domain.com/api/assess",
json=bundle
)
results = response.json()
for dx in results["diagnosis_results"]:
print(f"{dx['code']}: {dx['risk_level']} — {dx['reason']}")
const bundle = { /* your FHIR Bundle */ };
const response = await fetch("/api/assess", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(bundle)
});
const data = await response.json();
data.diagnosis_results.forEach(dx => {
console.log(`${dx.code}: ${dx.risk_level} — ${dx.reason}`);
});
Step 3 — Read the Response
Because the only encounter is ambulatory (AMB) — not inpatient — the engine flags the stroke diagnosis as high risk:
{
"status": "success",
"diagnosis_results": [
{
"code": "I63.9",
"description": "Cerebral infarction, unspecified",
"risk_level": "high",
"reason": "One stroke diagnosis on physician claim, no inpatient/outpatient claim"
}
]
}
reason tells you the stroke diagnosis appeared without a supporting inpatient stay. A coder should verify whether the patient was actually admitted. If not, the code may need to be changed to a "history of" code (e.g., Z86.73).
Step 4 — Fix and Re-submit
If you add an inpatient encounter and re-submit, the risk level drops:
{
"resource": {
"resourceType": "Encounter",
"id": "enc-inpatient-01",
"class": { "code": "IMP" },
"subject": { "reference": "Patient/pat-100" },
"period": {
"start": "2024-09-15",
"end": "2024-09-20"
}
}
}
New response:
{
"status": "success",
"diagnosis_results": [
{
"code": "I63.9",
"description": "Cerebral infarction, unspecified",
"risk_level": "low",
"reason": "No risk factors identified"
}
]
}
Best Practices
- Include all supporting resources. Encounters, medications, and procedures dramatically improve accuracy.
-
Set
onsetDateTimeon every Condition. Without it, time-window rules cannot fire. - Use standard code systems. Stick to the three recognized ICD-10 URIs listed in Section [2].
- Batch your submissions. Send all of a patient's resources in one Bundle for complete assessment.
- Review all HIGH results. Each comes with a specific, actionable reason.
Common Questions
/api/assess endpoint processes data in memory and returns results immediately. Nothing is persisted from direct API calls.
code, coding, onsetDateTime, etc.).
Ready to Try It?
See the engine in action with our interactive demo or explore pre-built test scenarios.
Interactive Demo High-Risk Scenarios API Reference