44 KiB
Patient Encounter API Lifecycle
Audience: API integrators, simulator authors, dashboard developers, and anyone who needs to understand how a patient moves through VigilCare from registration to discharge.
What is VigilCare? VigilCare is a clinical monitoring system that tracks patients in a hospital. It continuously watches vital signs (like heart rate, blood pressure, and temperature), calculates safety scores, and fires alerts when something looks wrong — so doctors and nurses can respond quickly.
Companion docs:
- API Reference — authoritative endpoint contracts and request bodies
- Medication correlation design — how drug context annotates warning/NEWS2 alerts
- Simulator guide — replay JSON scenarios against this lifecycle
- Dashboard guide — which endpoints the Vue ward UI polls
What this document covers
A hospital stay in VigilCare is called an encounter, and it belongs to a patient. Think of it this way: a patient is a person, and an encounter is one visit to the hospital. The same patient can have many encounters over their lifetime.
Everything clinical — measurements, alerts, medication records, lab orders, safety scores, sepsis bundles — is attached to an encounter while it is ACTIVE (meaning the patient is still in the hospital).
This guide walks through the lifecycle in order: what API calls to make, what happens right away in the response, what happens a few seconds later in the background, and what stops working after the patient leaves.
There is no single "admit patient" button. Instead, you combine the standard REST endpoints below.
Quick glossary of medical terms
Before we dive in, here are the medical terms you will see throughout this document:
| Term | What it means |
|---|---|
| Vital signs | Basic body measurements that show how a patient is doing — heart rate, blood pressure, temperature, breathing rate, oxygen level |
| Observation | Any measurement recorded for a patient (vitals, lab results, consciousness checks) |
| Sepsis | A life-threatening condition where the body's response to an infection starts damaging its own organs. It can kill quickly if not treated |
| NEWS2 | National Early Warning Score 2 — a scoring system that combines 7 vital signs into a single number (0–20) to detect patients who are getting sicker |
| SOFA | Sequential Organ Failure Assessment — a score (0–24) that measures how well 6 organ systems are working. Used to detect organ failure and sepsis |
| qSOFA | Quick SOFA — a fast bedside check using just 3 things (breathing rate, blood pressure, consciousness). Used as an early screen: "should we look more closely for sepsis?" |
| GCS | Glasgow Coma Scale — measures how conscious/awake a patient is (3–15). Lower is worse. A score of 3 means completely unresponsive, 15 means fully alert |
| AVPU | A simpler consciousness scale: Alert, responds to Voice, responds to Pain, Unresponsive |
| Threshold | A boundary value. If a measurement crosses a threshold, it triggers an alert |
| Alert | A notification to clinical staff that something needs attention — like an alarm going off |
| Sepsis bundle | A checklist of 4 things that must be done within 1 hour when sepsis is suspected (blood tests, antibiotics, IV fluids) |
| MRN | Medical Record Number — the human-readable ID for a patient (e.g. MRN-000042) |
Conventions
| Item | Value |
|---|---|
| Base path | /api/v1 |
| Response envelope | { success, statusCode, data, error } |
| Correlation | Optional X-Correlation-Id request header; echoed on the response |
| Active encounter guard | POST observations, medications, and orders return 409 (ENCOUNTER_NOT_ACTIVE) when the encounter is discharged or cancelled |
| Async latency | Warning alerts, NEWS2 scores, qSOFA, SOFA, trend alerts, and sepsis bundles are created by background workers — allow a few seconds after recording a measurement before polling for results |
Lifecycle overview
flowchart LR
subgraph admission [Admission]
A1[POST /patients]
A2[POST /patients/id/encounters]
end
subgraph monitoring [Active monitoring]
M1[POST /encounters/id/observations]
M2[POST /encounters/id/medications]
M3[POST /encounters/id/orders]
end
subgraph async [Background scoring engines]
K1[warning-evaluator]
K2[news2-scoring]
K3[sepsis-engine]
K4[trend-analyzer]
K5[sofa-scoring]
K6[gcs-scoring]
end
subgraph triage [Clinician response]
T1[GET /alerts]
T2[POST /alerts/id/acknowledge]
T3[POST /alerts/id/resolve]
T4[PATCH /orders/id/result]
end
subgraph discharge [Discharge]
D1[PATCH /encounters/id/status]
D2[encounter.status.changed → discharge summary]
end
A1 --> A2 --> M1
M1 --> K1 & K2 & K3 & K4 & K5 & K6
K1 & K2 & K3 & K4 & K5 & K6 --> T1
M2 & M3 --> T1
T1 --> T2 --> T3
M3 --> T4
T3 --> D1 --> D2
How encounter status works (enforced by PATCH /encounters/{id}/status):
scheduled → active → discharged
→ cancelled
POST /patients/{id}/encounters creates encounters directly in ACTIVE. Illegal transitions (like going from discharged back to active) return 409 (ILLEGAL_STATUS_TRANSITION).
All measurements the system tracks
VigilCare tracks measurements (called "observations") across three categories: vital signs, lab results, and consciousness assessments. Each measurement has a plausible range — values outside this range are rejected as likely errors.
Vital signs (measured at the bedside)
These are the basic measurements that nurses and devices collect regularly:
| Code | What it measures | Unit | Normal range (approx.) | Plausible range | Example |
|---|---|---|---|---|---|
HEART_RATE |
How fast the heart beats | bpm (beats per minute) | 60–100 | 1–300 | A resting adult is ~70 bpm. Over 150 or under 30 is critical |
RESP_RATE |
How many breaths per minute | breaths/min | 12–20 | 1–80 | Normal adults breathe 12–20 times/min. Over 30 is critical |
SYSTOLIC_BP |
Blood pressure (the top number) — pressure when the heart squeezes | mmHg | 90–140 | 40–300 | 120/80 is "normal." Under 70 systolic is critical (not enough blood reaching organs) |
DIASTOLIC_BP |
Blood pressure (the bottom number) — pressure when the heart relaxes | mmHg | 60–90 | 20–200 | The lower number in a blood pressure reading |
TEMP_C |
Body temperature | °C (Celsius) | 36.1–37.2 | 15–50 | Normal is ~37°C (98.6°F). Over 40°C or under 35°C is critical |
SPO2 |
Oxygen saturation — how much oxygen is in the blood | % | 95–100 | 50–100 | Measured with a clip on the finger. Below 88% is critical |
SUPPLEMENTAL_O2 |
Whether the patient is receiving extra oxygen | flag (0 or 1) | 0 (room air) | 0–1 | 0 = breathing normal air, 1 = on supplemental oxygen. Being on oxygen adds points to the NEWS2 score |
Lab results (from blood tests and other samples)
These require a blood draw or lab analysis — they take longer to get but reveal what is happening inside the body:
| Code | What it measures | Unit | Normal range (approx.) | Plausible range | Why it matters |
|---|---|---|---|---|---|
POTASSIUM_MEQ_L |
Potassium level in blood | mEq/L | 3.5–5.0 | 0.1–12 | Too high or too low can cause dangerous heart rhythms |
WBC_K_UL |
White blood cell count | k/uL (thousands per microliter) | 4.0–12.0 | 0.1–500 | White blood cells fight infection. Very high = possible infection. Very low = weak immune system |
GLUCOSE_MG_DL |
Blood sugar level | mg/dL | 70–100 (fasting) | 10–1000 | Too low (hypoglycemia) can cause seizures. Too high (hyperglycemia) damages organs over time |
LACTATE_MMOL_L |
Lactic acid in blood | mmol/L | 0.5–2.0 | 0.1–30 | High lactate means tissues are not getting enough oxygen — a key sign of sepsis or shock |
PAO2_MMHG |
Partial pressure of oxygen in arterial blood | mmHg | 80–100 | 20–600 | Requires an arterial blood draw. Shows how well lungs transfer oxygen. Used in SOFA respiratory scoring |
FIO2_PCT |
Fraction of inspired oxygen — how concentrated the oxygen the patient breathes is | % | 21 (room air) | 21–100 | Room air is 21%. A patient on a ventilator might be at 40–100%. Used with PaO2 to calculate SOFA respiratory score |
PLATELET_K_UL |
Platelet count | k/uL | 150–400 | 1–1500 | Platelets help blood clot. Very low platelets = bleeding risk. Used in SOFA coagulation scoring |
BILIRUBIN_MG_DL |
Bilirubin level | mg/dL | 0.1–1.2 | 0.1–50 | Produced when the liver breaks down old blood cells. High bilirubin = liver is struggling. Used in SOFA liver scoring |
CREATININE_MG_DL |
Creatinine level | mg/dL | 0.6–1.2 | 0.1–20 | A waste product filtered by the kidneys. High creatinine = kidneys are not filtering properly. Used in SOFA renal scoring |
URINE_OUTPUT_ML_H |
How much urine the patient produces per hour | mL/h | >50 | 0–500 | Low urine output = kidneys may be failing. Used in SOFA renal scoring |
Consciousness assessments
These measure how awake and responsive the patient is:
| Code | What it measures | Range | What the numbers mean |
|---|---|---|---|
AVPU |
Quick consciousness check | 0–3 | 0 = Alert (awake and talking), 1 = responds to Voice, 2 = responds to Pain only, 3 = Unresponsive. Used in NEWS2 scoring |
GCS_EYE |
Eye opening response | 1–4 | 4 = opens eyes on own, 3 = opens to voice, 2 = opens to pain, 1 = no eye opening |
GCS_VERBAL |
Verbal response | 1–5 | 5 = oriented/normal conversation, 4 = confused, 3 = inappropriate words, 2 = incomprehensible sounds, 1 = none |
GCS_MOTOR |
Motor (movement) response | 1–6 | 6 = obeys commands, 5 = localizes pain, 4 = withdraws from pain, 3 = abnormal flexion, 2 = extension, 1 = none |
GCS total (3–15) is the sum of all three components. A GCS of 15 means fully alert. A GCS of 3 means completely unresponsive.
Alert thresholds — when do alarms go off?
Every measurement is checked against configured thresholds. There are two levels:
- Critical — something is dangerously wrong right now. The alert fires immediately (in the same HTTP response). These are never suppressed.
- Warning — something is concerning and needs attention. The alert fires in the background (via Kafka). Warnings can be temporarily suppressed after a clinician acknowledges them (30-minute window).
| Measurement | Critical Low | Warning Low | Warning High | Critical High |
|---|---|---|---|---|
| Heart Rate (bpm) | 30 | 50 | 100 | 150 |
| Temperature (°C) | 35.0 | 36.0 | 38.3 | 40.0 |
| Potassium (mEq/L) | 2.5 | 3.5 | 5.0 | 6.5 |
| Oxygen Saturation (%) | 88 | 92 | — | — |
| Respiratory Rate (breaths/min) | — | 12 | 20 | 30 |
| White Blood Cells (k/uL) | 2.0 | 4.0 | 12.0 | 20.0 |
| Systolic BP (mmHg) | 70 | 90 | 160 | 180 |
| Diastolic BP (mmHg) | 40 | 60 | 90 | 110 |
| Lactate (mmol/L) | — | — | 2.0 | 4.0 |
| AVPU | — | — | — | 2 |
| Glucose (mg/dL) | 40 | 70 | 180 | 400 |
| PaO2 (mmHg) | 60 | 80 | — | — |
| Platelets (k/uL) | 20 | 50 | — | — |
| Bilirubin (mg/dL) | — | — | 2.0 | 6.0 |
| Creatinine (mg/dL) | — | — | 2.0 | 3.5 |
Example: A heart rate of 155 bpm crosses the critical high threshold (150), so a CRITICAL_HEART_RATE alert fires immediately. A heart rate of 105 bpm crosses only the warning high threshold (100), so a WARNING_HEART_RATE alert fires in the background.
A dash (—) means there is no threshold configured on that side. For example, oxygen saturation has no high threshold because high oxygen is generally not dangerous in this context.
Clinical scoring systems explained
VigilCare calculates several clinical scores automatically. Each score looks at patient data from a different angle. Together, they give clinicians a complete picture of how the patient is doing.
NEWS2 — National Early Warning Score 2
What it does: Combines 7 vital signs into a single number (0–20) that tells staff "how sick is this patient right now?" It is the most widely used early warning system in UK hospitals.
Why it matters: A rising NEWS2 score often means the patient is getting worse — even if no single measurement has crossed a critical threshold yet. It catches deterioration early.
The 7 required measurements:
All 7 must be recorded within a 4-hour window for a score to be calculated. If any are missing, no score is produced.
| Measurement | Score 3 | Score 2 | Score 1 | Score 0 (normal) | Score 1 | Score 2 | Score 3 |
|---|---|---|---|---|---|---|---|
| Respiratory Rate | ≤8 | 9–11 | 12–20 | 21–24 | ≥25 | ||
| Oxygen Saturation (%) | ≤91 | 92–93 | 94–95 | ≥96 | |||
| Systolic BP (mmHg) | ≤90 | 91–100 | 101–110 | 111–219 | ≥220 | ||
| Heart Rate (bpm) | ≤40 | 41–50 | 51–90 | 91–110 | 111–130 | ≥131 | |
| Consciousness (AVPU) | Alert | Any other (V, P, or U) | |||||
| Temperature (°C) | ≤35.0 | 35.1–36.0 | 36.1–38.0 | 38.1–39.0 | ≥39.1 | ||
| Supplemental O2 | On oxygen (2) | Room air (0) |
How the total score maps to risk levels:
| Total Score | Risk Level | What it means | Alert type |
|---|---|---|---|
| 0–4 | LOW | Routine monitoring. Patient is stable | No alert |
| 0–4 but any single parameter scores 3 | LOW-MEDIUM | One vital sign is individually concerning, even though the total looks OK | NEWS2_WARNING |
| 5–6 | MEDIUM | Patient may be deteriorating. Increase monitoring frequency | NEWS2_WARNING |
| 7 or higher | HIGH | Urgent — patient is at significant risk. Senior clinician review needed | NEWS2_EMERGENCY |
Example: A patient has: respiratory rate 24 (score 2), SpO2 94% (score 1), systolic BP 115 (score 0), heart rate 115 (score 2), AVPU alert (score 0), temperature 38.5°C (score 1), on room air (score 0). Total = 6 → MEDIUM risk → NEWS2_WARNING alert fires.
SOFA — Sequential Organ Failure Assessment
What it does: Scores how well 6 organ systems are functioning, on a scale of 0–4 each (total 0–24). Higher scores mean worse organ function. It is the gold standard for detecting sepsis-related organ damage.
Why it matters: Sepsis kills by damaging organs. SOFA catches this damage early. A jump of 2+ points from the patient's baseline is the clinical definition of sepsis (per Sepsis-3 guidelines).
The 6 organ systems scored:
| Organ System | What it measures | Data needed | Score 0 (normal) | Score 1 | Score 2 | Score 3 | Score 4 (worst) |
|---|---|---|---|---|---|---|---|
| Respiratory (lungs) | How well lungs transfer oxygen | PaO2 and FiO2 (or SpO2 if PaO2 unavailable) | PaO2/FiO2 ≥400 | 300–399 | 200–299 | 100–199 | <100 |
| Coagulation (blood clotting) | Platelet count | Platelet count | ≥150 k/uL | 100–149 | 50–99 | 20–49 | <20 |
| Liver | Liver function | Bilirubin | <1.2 mg/dL | 1.2–1.9 | 2.0–5.9 | 6.0–11.9 | ≥12.0 |
| Cardiovascular (heart/circulation) | Blood pressure support needed | MAP (mean arterial pressure) | MAP ≥70 | MAP <70 | Low-dose vasopressor | Moderate vasopressor | High-dose vasopressor |
| CNS (brain) | Consciousness level | GCS total score | 15 | 13–14 | 10–12 | 6–9 | <6 |
| Renal (kidneys) | Kidney function | Creatinine and/or urine output | Creatinine <1.2 | 1.2–1.9 | 2.0–3.4 | 3.5–4.9 | ≥5.0 or urine <200 mL/day |
How SOFA alerts work:
The system establishes a "baseline" SOFA score once at least 4 of the 6 organ systems have data. Then it watches for changes:
| Change from baseline | Alert | Severity | What it means |
|---|---|---|---|
| Increase of 2+ points | SOFA_SEPSIS |
CRITICAL | Meets the Sepsis-3 definition of sepsis. Immediate clinical response needed |
| Increase of 1 point | SOFA_WARNING |
WARNING | Organs are trending worse. Watch closely |
Lab staleness: SOFA uses lab results that may not be measured frequently. The system considers lab values stale after 12 hours and expired after 24 hours, meaning the score becomes less reliable over time without fresh data.
Example: A patient has a baseline SOFA of 3. New labs come in showing worsening kidney function and lower platelets, pushing the SOFA to 6. That is a jump of 3 points → SOFA_SEPSIS alert fires.
qSOFA — Quick SOFA (bedside screening)
What it does: A fast, simple check using just 3 things you can measure at the bedside — no lab work needed. It asks: "should we be worried about sepsis?"
Why it matters: Lab results take time. qSOFA gives an answer in seconds using measurements that are already being collected. It is a screening tool — it does not diagnose sepsis, but it flags patients who need deeper evaluation (like a full SOFA score).
The 3 criteria:
| Criteria | Threshold | What it means |
|---|---|---|
| Respiratory rate | ≥22 breaths/min | Patient is breathing faster than normal — body may be compensating for something |
| Systolic blood pressure | ≤100 mmHg | Blood pressure is dropping — organs may not be getting enough blood |
| Altered mental status | AVPU ≥1 (not fully alert) or GCS <15 | Patient is confused or not fully conscious |
Scoring: Each criterion met = 1 point. Range is 0–3. When 2 or more criteria are met, a QSOFA_SCREEN alert fires.
Important: qSOFA is a screen, not a diagnosis. It says "look closer" — it does not by itself trigger a sepsis bundle. The full SOFA score is used for sepsis determination.
Example: A patient has a respiratory rate of 24 (meets criterion), systolic BP of 95 (meets criterion), and is alert (AVPU = 0, does not meet criterion). Score = 2 → QSOFA_SCREEN alert fires.
GCS — Glasgow Coma Scale
What it does: Measures how conscious a patient is by testing three types of responses: eye opening, verbal response, and motor (movement) response. Total score ranges from 3 (completely unresponsive) to 15 (fully alert).
Why it matters: Changes in consciousness can signal brain injury, stroke, medication effects, or worsening illness. A dropping GCS is an emergency.
The three components:
| Component | Best response (highest score) | Worst response (lowest score) |
|---|---|---|
| Eye opening | 4 — opens eyes spontaneously | 1 — no eye opening |
| Verbal response | 5 — oriented, normal conversation | 1 — no verbal response |
| Motor response | 6 — obeys commands | 1 — no movement |
Severity classification:
| GCS Total | Classification | Alert | What it means |
|---|---|---|---|
| 13–15 | Mild | No alert | Patient is mostly or fully alert |
| 9–12 | Moderate | GCS_WARNING |
Significant impairment — needs monitoring |
| 3–8 | Severe (coma) | GCS_CRITICAL |
Patient is in or near coma — urgent intervention needed |
How GCS connects to other scores:
- NEWS2: GCS 15 maps to AVPU = "Alert" (score 0). Any GCS below 15 maps to "not alert" (score 3 in NEWS2). This is why even a small drop in consciousness adds 3 points to NEWS2.
- qSOFA: GCS below 15 counts as "altered mental status" — one of the 3 qSOFA criteria.
- SOFA CNS component: GCS maps directly to the SOFA brain score (15→0, 13–14→1, 10–12→2, 6–9→3, <6→4).
Trend detection — Rapid Deterioration
What it does: Watches how fast certain vital signs are changing over time, not just whether they have crossed a threshold. Even if a value is still in a "normal" range, a rapid change can signal trouble.
Why it matters: A heart rate going from 70 to 100 in 30 minutes is more alarming than a stable heart rate of 100. The trend tells you the patient is getting worse fast.
Monitored vital signs and velocity thresholds:
The system looks at changes over a 30-minute sliding window:
| Vital Sign | Trigger rate | In plain terms | Alert |
|---|---|---|---|
| Heart Rate | ≥0.5 bpm/min | Rising by 15+ bpm in 30 minutes | RAPID_DETERIORATION |
| Respiratory Rate | ≥0.3 breaths/min | Rising by 9+ breaths/min in 30 minutes | RAPID_DETERIORATION |
| Systolic BP | ≥1.0 mmHg/min decline | Dropping by 30+ mmHg in 30 minutes | RAPID_DETERIORATION |
| Temperature | ≥0.05 °C/min | Rising by 1.5+°C in 30 minutes | RAPID_DETERIORATION |
| Oxygen Saturation | ≥0.2 %/min decline | Dropping by 6+% in 30 minutes | RAPID_DETERIORATION |
Example: A patient's heart rate readings: 75 bpm at 2:00 PM, 82 bpm at 2:10 PM, 92 bpm at 2:25 PM. That is a rise of 17 bpm in 25 minutes (0.68 bpm/min) — above the 0.5 threshold → RAPID_DETERIORATION alert fires, even though 92 bpm is not above the warning threshold (100) yet.
RAPID_DETERIORATION alerts are never suppressed — every rapid change triggers a new alert.
Phase 0 — Platform prerequisites (before any patient)
These are typically set up once at deploy time, not called per patient.
| What | How | Why it matters |
|---|---|---|
| Alert thresholds | Seeded in PostgreSQL; loaded into Redis on startup; manageable via POST/GET/PUT /alert-thresholds |
Every measurement is validated against configured thresholds to decide if alerts fire |
| Kafka topics | Provisioned by KafkaTopicProvisioner |
observation.recorded, alert.generated, encounter.status.changed (+ sepsis bundle topics) |
| Drug-vital mappings | MedicationCorrelation section in appsettings.json |
Links medications to vital signs so alerts can include context like "heart rate may be elevated due to epinephrine given 20 min ago" |
Phase 1 — Patient arrives (registration and admission)
1.1 Register the patient
POST /api/v1/patients
| Field | Required | Notes |
|---|---|---|
firstName, lastName, dateOfBirth, gender |
yes | |
bloodType, allergies, emergencyContactName, emergencyContactPhone |
no | Stored for ward context |
Response: 201 Created — data includes a system-generated mrn (e.g. MRN-000042) and id (UUID). Save both; the MRN is what nurses see on wristbands, the UUID is used in all API paths.
Later lookups:
| Need | Endpoint |
|---|---|
| Search by name or MRN | GET /patients?q=... |
| Demographics + active encounter summary | GET /patients/{id} |
1.2 Open an encounter (admission)
POST /api/v1/patients/{patientId}/encounters
| Field | Required | Notes |
|---|---|---|
encounterType |
yes | INPATIENT, OUTPATIENT, EMERGENCY |
department |
yes | e.g. ICU, GENERAL_MEDICINE, SURGERY |
attendingPhysician |
yes | |
roomBed |
no | Ward assignment (e.g. ICU-1A) |
admissionReason |
no | Why the patient was admitted |
Response: 201 Created — encounter id, status: ACTIVE, admittedAt.
Side effects:
- Event
encounter.status.changed→ Kafka → updates Elasticsearch ward index, data lake, and discharge-summary queue - Only one active encounter per patient per encounter type — trying to create a duplicate returns 409 (
DUPLICATE_ACTIVE_ENCOUNTER)
From this point, encounterId is the key for all clinical writes.
Phase 2 — Ward visibility (who is on the floor)
While the patient is active, ward systems poll aggregated state.
Virtual ward board
GET /api/v1/encounters?status=ACTIVE&department=ICU&page=1&pageSize=20
Each row (WardEncounterSummary) includes:
| Field | What it shows |
|---|---|
encounterId, patientId, mrn, firstName, lastName, roomBed, department, status |
Patient and encounter info |
news2Score, news2RiskLevel |
Latest NEWS2 score and risk level (LOW, LOW_MEDIUM, MEDIUM, HIGH) |
qsofaScore |
Current qSOFA criteria count (0–3) |
sepsisActive |
Whether the patient has a sepsis investigation in progress |
sepsisBundleStatus |
IN_PROGRESS, COMPLIANT, or NON_COMPLIANT |
openAlertCount |
How many unresolved alerts exist for this patient |
The dashboard sorts patients by NEWS2 score — sickest patients appear first.
Single-patient chart header
GET /api/v1/encounters/{id}
Returns the encounter with the patient's info, the 10 most recent observations, and open alerts. Use this for a quick summary; use the dedicated list endpoints for full history.
Phase 3 — Continuous monitoring (recording measurements)
Observations are the heartbeat of the system. Bedside devices, manual nurse entries, and lab interfaces all use the same endpoint.
Record measurements
POST /api/v1/encounters/{encounterId}/observations
Body: { "observations": [ ... ] } — 1 to 10 measurements per call.
| Observation field | Required | Notes |
|---|---|---|
observationCode |
yes | One of the measurement codes listed above (e.g. HEART_RATE, RESP_RATE) |
value, unit |
yes | The numeric value and its unit. Checked for plausibility (e.g. a heart rate of 500 would be rejected) |
recordedAt |
yes | When the measurement was actually taken |
source |
no | DEVICE (default), MANUAL, LAB |
Optional header: Idempotency-Key — if you accidentally send the same measurement twice, the system returns the original record without creating a duplicate.
What happens immediately (in the HTTP response)
- Encounter must be
ACTIVE— otherwise 409 - Idempotency check — skip if already recorded
- Plausibility validation — reject impossible values with 422
- Save the measurement
- Check against critical thresholds from Redis
- If a critical threshold is breached: create an alert immediately in the same database transaction
- Queue the measurement for background processing
- Commit
HTTP response (201): for each measurement, you get observation, alertGenerated (true only for critical alerts), alertId, and duplicate.
What happens in the background (not in the HTTP response)
Every recorded measurement is processed independently by these background engines:
| Engine | What it does | Alert produced |
|---|---|---|
| warning-evaluator | Checks if the value crosses a warning threshold. Optionally adds medication context (e.g. "patient was given morphine 30 min ago") | WARNING_* alerts |
| news2-scoring | Recalculates NEWS2 if all 7 parameters are available within 4 hours | NEWS2_WARNING or NEWS2_EMERGENCY |
| sepsis-engine | Recalculates qSOFA criteria count | QSOFA_SCREEN (when 2+ criteria met) |
| sofa-scoring | Recalculates SOFA organ scores using latest vitals and labs | SOFA_WARNING or SOFA_SEPSIS |
| gcs-scoring | Recalculates GCS when eye/verbal/motor components are recorded | GCS_WARNING or GCS_CRITICAL |
| trend-analyzer | Checks if the vital sign is changing too fast (rate of change over 30-min window) | RAPID_DETERIORATION |
| es-indexer | Updates Elasticsearch for search and analytics | (no alert) |
| data-lake-writer | Writes Parquet files to MinIO for long-term analysis | (no alert) |
Important for integrators: After posting measurements, poll GET /encounters/{id}/alerts or score endpoints. Warning and scoring alerts are NOT in the initial HTTP response — they arrive 1–3 seconds later.
Read measurement history
GET /api/v1/encounters/{encounterId}/observations?code=HEART_RATE&from=...&to=...&limit=50&cursor=...
Cursor-paginated (newest first). Use code to filter by measurement type.
Phase 4 — Clinical interventions (medications and orders)
These run in parallel with monitoring; they do not replace recording measurements.
Medication administrations
POST /api/v1/encounters/{encounterId}/medications
GET /api/v1/encounters/{encounterId}/medications?since=...&page=1&pageSize=20
GET /api/v1/medications/{id}
Recording a drug does not create alerts on its own. Instead, when a warning or NEWS2 alert fires later, the system checks: "was a relevant drug given recently?" If so, it adds context to the alert details. For example, a high heart rate warning might include: "Note: epinephrine administered 20 minutes ago."
The correlation window is 90 minutes by default — only drugs given within the last 90 minutes are linked.
Clinical orders
POST /api/v1/encounters/{encounterId}/orders
GET /api/v1/encounters/{encounterId}/orders?status=PENDING
GET /api/v1/orders/{id}
PATCH /api/v1/orders/{id}/status
PATCH /api/v1/orders/{id}/result
Order status flow:
pending → in_progress → resulted
→ cancelled
PATCH .../result is how clinicians record that an order is complete — for example, recording that a blood culture was collected. For orders that are part of a sepsis bundle, completing the order also marks that bundle element as done.
Phase 5 — Alerts fire (detection to triage)
Alerts are created by multiple engines (critical thresholds, warning thresholds, NEWS2, qSOFA, SOFA, GCS, trend detection). All share the same read and lifecycle API.
List and inspect
| Endpoint | Use |
|---|---|
GET /encounters/{id}/alerts?status=OPEN |
Alerts for one patient |
GET /alerts?status=OPEN&severity=CRITICAL&department=ICU |
Hospital-wide alert center |
GET /alerts/{id} |
Single alert with full details |
Each alert includes alertType, severity, details (with medication context when applicable), status, triggeredAt, and optional observationId (which measurement triggered it).
All alert types:
| Alert Type | What triggered it | Severity | Can be suppressed? |
|---|---|---|---|
CRITICAL_* (e.g. CRITICAL_HEART_RATE) |
A measurement crossed a critical threshold | CRITICAL | No |
WARNING_* (e.g. WARNING_HEART_RATE) |
A measurement crossed a warning threshold | WARNING | Yes (30 min) |
NEWS2_WARNING |
NEWS2 score is 5–6, or any single parameter scores 3 | WARNING | Yes (30 min) |
NEWS2_EMERGENCY |
NEWS2 score is 7 or higher | CRITICAL | No |
QSOFA_SCREEN |
2 or more qSOFA criteria met | WARNING | Yes (30 min) |
SOFA_SEPSIS |
SOFA score increased 2+ points from baseline | CRITICAL | No |
SOFA_WARNING |
SOFA score increased 1 point from baseline | WARNING | Yes (30 min) |
GCS_CRITICAL |
GCS total is 8 or below (coma) | CRITICAL | No |
GCS_WARNING |
GCS total is 9–12 (moderate impairment) | WARNING | Yes (30 min) |
RAPID_DETERIORATION |
A vital sign is changing too fast | WARNING | No |
Acknowledge and resolve
POST /api/v1/alerts/{id}/acknowledge
Body: { "clinicianId": "DR-SMITH", "note": "optional" }
POST /api/v1/alerts/{id}/resolve
Alert lifecycle:
open → acknowledged → resolved
→ escalated (unacknowledged CRITICAL after 5 min → pages on-call staff)
When a clinician acknowledges a suppressible warning, a 30-minute suppression window starts in Redis. During that window, duplicate warnings of the same type for the same patient will not fire again. Critical and SOFA-sepsis alerts are never suppressed.
Escalation (no HTTP endpoint)
Critical alerts that remain unacknowledged for 5+ minutes are automatically escalated — they are routed to an on-call paging queue. This happens at the infrastructure level; integrators see it through the alert's status field changing to escalated.
Phase 6 — Composite scores (NEWS2, SOFA, qSOFA, GCS)
NEWS2
Requires all 7 parameters in Redis within a 4-hour window: RESP_RATE, SPO2, SYSTOLIC_BP, HEART_RATE, AVPU, TEMP_C, SUPPLEMENTAL_O2.
GET /api/v1/encounters/{id}/news2/current → latest score (404 if not yet computed)
GET /api/v1/encounters/{id}/news2/history → score history over time
Scores and NEWS2 alerts are calculated in the background — record the 7th parameter, wait a few seconds, then poll.
SOFA
Requires lab results and vitals across 6 organ systems. Not all systems need data immediately — the score is computed with whatever is available once at least 4 systems have data.
GET /api/v1/encounters/{id}/sofa/current → latest SOFA score with per-organ breakdown
GET /api/v1/encounters/{id}/sofa/history → score history
The response includes per-organ scores (respiratory, coagulation, liver, cardiovascular, CNS, renal) so clinicians can see which organ systems are struggling.
qSOFA
Live criteria count from Redis (no stored history — just the current state):
GET /api/v1/encounters/{id}/qsofa/current
Returns how many of the 3 criteria are currently met (0–3). When the count reaches 2, a QSOFA_SCREEN alert fires.
GCS
GET /api/v1/encounters/{id}/gcs/current → latest GCS with component breakdown
Returns the total GCS score, individual component scores (eye, verbal, motor), and severity classification (mild/moderate/severe).
Phase 7 — Sepsis bundle (when infection is suspected)
A sepsis bundle is a checklist of things that must be done within 1 hour when sepsis is suspected. VigilCare creates bundles automatically when a SOFA_SEPSIS alert fires — not by a direct API call.
GET /api/v1/encounters/{id}/sepsis-bundle/current
GET /api/v1/sepsis-bundles/{id}
The 4 bundle elements (each backed by an auto-created clinical order):
| Element | What it is | Why within 1 hour |
|---|---|---|
| Blood cultures | Draw blood samples to identify which bacteria is causing the infection | Need to identify the infection before antibiotics potentially mask it |
| Serum lactate | Measure lactic acid level in blood | High lactate means organs are not getting enough oxygen — guides how aggressive treatment needs to be |
| Broad-spectrum antibiotics | Give antibiotics that cover many types of bacteria | Every hour of delay increases mortality risk. Start broad, then narrow down when culture results come back |
| IV fluid resuscitation | Give fluids through an IV to restore blood pressure and organ perfusion | Sepsis causes blood vessels to leak — fluids keep blood pressure up and organs working |
Compliance flow:
- Bundle is created with
complianceStatus: IN_PROGRESSanddeadlineAtset to 1 hour from detection - Clinicians complete orders via
PATCH /orders/{id}/result - Each completed order marks its bundle element
COMPLETED - When all 4 elements are done:
- Within deadline →
COMPLIANT(good) - Past deadline →
NON_COMPLIANT(needs review — delay may have worsened outcome)
- Within deadline →
- A background monitor checks every 5 minutes and marks overdue bundles as
NON_COMPLIANT
GET /api/v1/encounters/{id}/orders
Use this to see bundle-linked orders and their status during the stay.
Phase 8 — Review during the stay (timeline and analytics)
Encounter timeline
GET /api/v1/encounters/{id}/timeline
A combined chronological stream of observations and alerts — like a scrolling history of everything that has happened to the patient. Useful for reviewing events during ward rounds or shift handovers.
Elasticsearch analytics (read-only, cross-encounter)
| Endpoint | Purpose |
|---|---|
GET /analytics/patients?q=...&department=... |
Search patients and encounters |
GET /analytics/observations/trend?encounterId=...&code=... |
Hourly avg/min/max time series for a measurement |
GET /analytics/alerts/summary?severity=...&department=... |
Alert volume by department |
GET /analytics/population?code=...&threshold=... |
Find patients above/below a measurement threshold |
These read from a search index fed by Kafka — not from the live database. There may be a few seconds of delay.
Phase 9 — Discharge (end of encounter)
Close the encounter
PATCH /api/v1/encounters/{encounterId}/status
Body: { "status": "DISCHARGED", "dischargeDiagnosis": "optional free text" }
Response: 200 OK — { encounterId, newStatus, dischargeDiagnosis }.
Side effects:
dischargedAtis set to the current time- Event fires → Kafka → Elasticsearch status update, data lake, and a discharge summary worker that generates a PDF stored at
/discharge-summaries/{encounterId}/summary.pdf
What stops working after discharge
| Action | Result |
|---|---|
POST .../observations |
409 ENCOUNTER_NOT_ACTIVE — cannot record new measurements |
POST .../medications |
409 ENCOUNTER_NOT_ACTIVE — cannot record new medications |
POST .../orders |
409 ENCOUNTER_NOT_ACTIVE — cannot create new orders |
GET .../alerts, GET .../observations, GET .../news2/history |
Still work — you can read historical data |
GET /encounters?status=ACTIVE |
Patient drops off the active ward list |
Cancellation: PATCH .../status with { "status": "CANCELLED" } from ACTIVE (e.g. an admission was created by mistake). Same write restrictions apply afterward.
A patient may later return and receive a new encounter via POST /patients/{id}/encounters. Each hospital visit is independent; the patient's MRN stays the same across their lifetime.
Complete endpoint index by lifecycle phase
| Phase | Method | Path |
|---|---|---|
| 0 — Config | GET/POST/PUT |
/alert-thresholds, /alert-thresholds/{id} |
| 1 — Admission | POST |
/patients |
POST |
/patients/{id}/encounters |
|
GET |
/patients, /patients/{id} |
|
| 2 — Ward | GET |
/encounters, /encounters/{id} |
GET |
/encounters/{id}/qsofa/current |
|
| 3 — Monitoring | POST |
/encounters/{id}/observations |
GET |
/encounters/{id}/observations |
|
| 4 — Interventions | POST/GET |
/encounters/{id}/medications, /medications/{id} |
POST/GET/PATCH |
/encounters/{id}/orders, /orders/{id}, /orders/{id}/status, /orders/{id}/result |
|
| 5 — Alerts | GET |
/encounters/{id}/alerts, /alerts, /alerts/{id} |
POST |
/alerts/{id}/acknowledge, /alerts/{id}/resolve |
|
| 6 — Scores | GET |
/encounters/{id}/news2/current, /encounters/{id}/news2/history |
GET |
/encounters/{id}/sofa/current, /encounters/{id}/sofa/history |
|
GET |
/encounters/{id}/qsofa/current |
|
GET |
/encounters/{id}/gcs/current |
|
| 7 — Sepsis | GET |
/encounters/{id}/sepsis-bundle/current, /sepsis-bundles/{id} |
| 8 — Review | GET |
/encounters/{id}/timeline |
GET |
/analytics/patients, /analytics/observations/trend, /analytics/alerts/summary, /analytics/population |
|
| 9 — Discharge | PATCH |
/encounters/{id}/status |
Operational (not encounter-scoped): GET /metrics (Prometheus), Swagger UI (development only).
Reference walkthrough — inpatient stay (step by step)
Here is the minimum sequence an integrator or simulator follows for a typical hospital stay:
# 1. Admit the patient
POST /api/v1/patients ← register the person
POST /api/v1/patients/{patientId}/encounters ← start their hospital visit
# 2. Record baseline vitals (first set of measurements)
POST /api/v1/encounters/{encounterId}/observations ← heart rate, BP, temp, etc.
# 3. Give medications and order tests
POST /api/v1/encounters/{encounterId}/medications ← record drugs given
POST /api/v1/encounters/{encounterId}/orders ← order blood tests, cultures, etc.
# 4. Ongoing monitoring (repeat during the stay)
POST /api/v1/encounters/{encounterId}/observations ← new vitals every few hours
GET /api/v1/encounters/{encounterId}/alerts ← check for alerts (wait 1-3s after posting vitals)
GET /api/v1/encounters/{encounterId}/news2/current ← check NEWS2 score
GET /api/v1/encounters/{encounterId}/sofa/current ← check SOFA score
GET /api/v1/encounters/{encounterId}/qsofa/current ← check qSOFA screen
GET /api/v1/encounters/{encounterId}/gcs/current ← check consciousness level
GET /api/v1/encounters/{encounterId}/sepsis-bundle/current ← check sepsis bundle if active
POST /api/v1/alerts/{alertId}/acknowledge ← clinician acknowledges an alert
PATCH /api/v1/orders/{orderId}/result ← record order completion
# 5. Ward round (overview)
GET /api/v1/encounters?status=ACTIVE ← see all active patients
GET /api/v1/encounters/{encounterId} ← patient summary
GET /api/v1/encounters/{encounterId}/timeline ← full event history
# 6. Discharge
PATCH /api/v1/encounters/{encounterId}/status { "status": "DISCHARGED", "dischargeDiagnosis": "..." }
For realistic clinical narratives (UTI progressing to sepsis, medication-related false alarms, rapid deterioration), replay the JSON scenarios in VigilCare.Simulator/Scenarios/List/ — each scenario file walks through this lifecycle with real clinical data.
Dashboard mapping
The Vue ward dashboard (vigilcare-dashboard/) implements this lifecycle through polling:
| UI screen | Primary API calls |
|---|---|
| Virtual Ward | GET /encounters?status=ACTIVE |
| Patient Detail | GET /encounters/{id}, GET .../news2/current, GET .../sofa/current, GET .../medications, GET .../orders, GET .../sepsis-bundle/current, GET .../observations, GET .../news2/history |
| Alert Center | GET /alerts?status=OPEN |
| Alert actions | POST /alerts/{id}/acknowledge, POST /alerts/{id}/resolve |
| Alert reasoning | alert.details from alert list + client-side medication window from GET .../medications |
Sync vs async quick reference
| Event | In the HTTP response? | Where to check afterward |
|---|---|---|
| Measurement saved | Yes (201) |
GET .../observations |
| Critical threshold alert | Yes (alertGenerated: true) |
GET .../alerts |
| Warning threshold alert | No — background (Kafka) | GET .../alerts (after ~1–3 s) |
| NEWS2 score / alert | No — background (Kafka) | GET .../news2/current, GET .../alerts |
| SOFA score / alert | No — background (Kafka) | GET .../sofa/current, GET .../alerts |
| qSOFA screen | No — background (Kafka) | GET .../alerts, GET .../qsofa/current |
| GCS score / alert | No — background (Kafka) | GET .../gcs/current, GET .../alerts |
| Trend alert | No — background (Kafka) | GET .../alerts |
| Medication annotation on alert | No — applied at alert creation | GET .../alerts → read details field |
| Discharge summary PDF | No — background (RabbitMQ) | MinIO /discharge-summaries/{encounterId}/summary.pdf |
How it all fits together — a real example
Imagine a patient named Maria arrives at the Emergency Department with a suspected urinary tract infection (UTI):
-
Admission: A clerk registers Maria (
POST /patients) and opens an emergency encounter (POST /patients/{id}/encounterswithencounterType: EMERGENCY). -
First vitals: A nurse records heart rate 95, respiratory rate 20, BP 115/75, temperature 38.8°C, SpO2 97%, AVPU = Alert, room air. No critical thresholds are crossed, so no immediate alerts. In the background, NEWS2 calculates: total = 3 (temp score 1 + heart rate score 1 + resp rate score 0 + others 0 + temperature gives 1) → LOW risk, no alert.
-
Labs ordered: The doctor orders blood cultures, lactate, and a CBC (
POST /encounters/{id}/orders). Broad-spectrum antibiotics are administered (POST /encounters/{id}/medications). -
Two hours later — getting worse: New vitals come in: heart rate 118, respiratory rate 24, BP 95/60, temperature 39.5°C, SpO2 94%. The
WARNING_SYSTOLIC_BPfires (below 100). NEWS2 recalculates to 8 → HIGH risk →NEWS2_EMERGENCYalert fires. qSOFA sees respiratory rate ≥22 and systolic BP ≤100 → 2 criteria →QSOFA_SCREENfires. -
Lab results arrive: Lactate comes back at 3.2 mmol/L (high — tissue hypoxia). Creatinine is 2.1 mg/dL (elevated — kidneys struggling). SOFA score jumps by 3 points from baseline →
SOFA_SEPSISalert fires. A sepsis bundle is automatically created with a 1-hour deadline. -
Sepsis bundle completion: The team draws blood cultures, gives IV fluids, and confirms antibiotics were already given. Each order is completed via
PATCH /orders/{id}/result. All 4 elements complete within the deadline → bundle status:COMPLIANT. -
Stabilization and discharge: Over the next 48 hours, vitals normalize. NEWS2 drops back to 2. SOFA returns to baseline. Maria is discharged with
PATCH /encounters/{id}/status.
Throughout this story, the ward board showed Maria's deterioration in real time — her NEWS2 score rising, qSOFA flagging, SOFA alerting — giving the clinical team the information they needed to act fast.