Urogram HL7v2 API (1.4.1)
Integration guide and API reference for hospital CIS/EMR systems connecting to the Urogram HL7v2 API for real-time urine monitoring data.
The Urogram HL7v2 API provides urine volume measurements and diuresis calculations as standard HL7 v2 ORU^R01 messages over HTTP. Two profiles are available:
| Profile | Endpoint | HL7 Version | Use Case |
|---|---|---|---|
| IHE PCD-01 aligned | /hl7v2/v1/pcd01/ORU/ | v2.6 | PCD-01 message shape with IEEE 11073 MDC nomenclature |
| Generic ORU | /hl7v2/v1/generic/ORU/ | v2.3 | General CIS integration (MetaVision, IntelliVue, Dräger, etc.) |
All data is automatically scoped to your hospital based on your API credentials.
This API is pull-based: your system authenticates with OAuth2 and polls the ORU endpoints over HTTPS, and Urogram returns the ORU^R01 message as an ER7 payload in the HTTP response body. There is no listener to configure on your side, and no message is sent to you unsolicited. See Recommended Polling Strategy for the polling pattern we recommend.
Traditional IHE PCD-01 is push-based — a Device Observation Reporter opens an MLLP connection to the consumer's listener (Mirth Connect / Open Integration Engine, Rhapsody, or similar) and sends each ORU^R01 with ACK handling.
Capability on request: Push delivery of ORU^R01 over MLLP is not part of the product today — the endpoints described in this document are the whole of the shipped HL7v2 interface. We can implement MLLP push against your listener if your integration requires it. It is a scoped engagement rather than a configuration switch (TCP connection management, retry/backoff, ACK^R01 parsing, and per-hospital endpoint configuration), so please raise it with us early — see Support — and we will confirm feasibility and timeline before you commit to a design.
Validate your integration against the Test environment before you go near production. It is not self-service: we have to create a hospital tenant for you there and issue credentials scoped to it, so ask us first — see Support. Tell us roughly how many devices you want to see and we will seed the tenant accordingly.
Test credentials are separate from production credentials and are not interchangeable. Everything else — endpoints, message formats, OAuth2 flow — is identical, so the only thing that should change when you cut over is the base URL and the client secret.
Two calls and you have data. Both work as-is once you substitute your credentials.
Step 1 — get a token
curl -sS -X POST https://app.htgmedical.com/oauth/v1/token/ \ -d grant_type=client_credentials \ -d client_id=YOUR_CLIENT_ID \ -d client_secret=YOUR_CLIENT_SECRET \ -d scope=m2m.read
{
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9...",
"expires_in": 36000,
"token_type": "Bearer",
"scope": "m2m.read"
}
Step 2 — pull a day of observations
Paste the access_token from step 1. Adjust the two timestamps to a window you expect to contain data.
curl -sS -H "Authorization: Bearer PASTE_ACCESS_TOKEN_HERE" \ "https://app.htgmedical.com/hl7v2/v1/generic/ORU/?date_from=2026-03-15T00:00:00Z&date_to=2026-03-16T00:00:00Z"
The response body is one ORU^R01 message per device, concatenated — split it on MSH|. An empty body means no matching observations, which is a normal answer rather than an error; see Troubleshooting if you expected data.
If you prefer a one-liner and have jq available:
TOKEN=$(curl -sS -X POST https://app.htgmedical.com/oauth/v1/token/ \ -d grant_type=client_credentials -d client_id=YOUR_CLIENT_ID \ -d client_secret=YOUR_CLIENT_SECRET -d scope=m2m.read | jq -r .access_token) curl -sS -H "Authorization: Bearer $TOKEN" \ "https://app.htgmedical.com/hl7v2/v1/generic/ORU/?date_from=2026-03-15T00:00:00Z&date_to=2026-03-16T00:00:00Z"
Where to go next
GET /hl7v2/v1/wards/— discover ward UUIDs if you want to filter by ward- Swap
genericforpcd01in the path to see the MDC-coded variant - Add
updated_sinceand switch to incremental polling — see Data Synchronization. Do this before you go live; adate_from-only poll silently misses late-arriving data.
The API uses OAuth2 Client Credentials flow — the same mechanism as the FHIR R4 API. If you already have FHIR credentials, they work here too.
POST /oauth/v1/token/ HTTP/1.1 Host: app.htgmedical.com Content-Type: application/x-www-form-urlencoded grant_type=client_credentials&client_id=YOUR_CLIENT_ID&client_secret=YOUR_CLIENT_SECRET&scope=m2m.read
cURL example:
curl -X POST https://app.htgmedical.com/oauth/v1/token/ \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=client_credentials" \ -d "client_id=YOUR_CLIENT_ID" \ -d "client_secret=YOUR_CLIENT_SECRET" \ -d "scope=m2m.read"
Response:
{
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9...",
"expires_in": 36000,
"token_type": "Bearer",
"scope": "m2m.read"
}
Tokens expire after 10 hours (expires_in: 36000 seconds). Cache the token and reuse it for every poll; request a new one shortly before expiry, or once after a 401 (requirement 2 under Integration Requirements):
import time
import requests
BASE_URL = "https://app.htgmedical.com"
class UrogramHL7Client:
def __init__(self, client_id, client_secret):
self.client_id = client_id
self.client_secret = client_secret
self.token = None
self.token_expires_at = 0.0
def _fetch_token(self):
response = requests.post(
f"{BASE_URL}/oauth/v1/token/",
data={
"grant_type": "client_credentials",
"client_id": self.client_id,
"client_secret": self.client_secret,
"scope": "m2m.read",
},
timeout=30,
)
response.raise_for_status()
data = response.json()
self.token = data["access_token"]
self.token_expires_at = time.monotonic() + data["expires_in"]
def get_oru(self, profile, params):
# Reuse the cached token; renew it 5 minutes before it expires
if not self.token or time.monotonic() >= self.token_expires_at - 300:
self._fetch_token()
url = f"{BASE_URL}/hl7v2/v1/{profile}/ORU/"
response = requests.get(
url, params=params, headers={"Authorization": f"Bearer {self.token}"}, timeout=60
)
if response.status_code == 401: # revoked or expired early: renew once, retry once
self._fetch_token()
response = requests.get(
url, params=params, headers={"Authorization": f"Bearer {self.token}"}, timeout=60
)
response.raise_for_status() # 429/5xx: back off and retry, see requirements 4-6
return response.text
These requirements apply to every Urogram integration, whether it uses the FHIR R4 API or the HL7v2 API. MUST marks what a safe integration needs; SHOULD marks what we strongly recommend, so deviate only for a reason you have agreed with us. Where a requirement depends on API-specific parameters or fields, the named section of this document has them.
- MUST keep the
client_secretin a secret store, never in source control, logs or URLs. It grants read access to your hospital's data. - MUST cache the access token and reuse it for every request until shortly before
expires_inelapses (for example, renew 5 minutes early). On a401, request one new token and retry once. MUST NOT request a new token for every poll. Each token request creates a server-side token; tokens last 10 hours. See Authentication for a caching example. - MUST NOT retry a
403in a loop. It means the token lacks them2m.readscope or the application is not linked to a hospital, which only a configuration change fixes; contact us.
- MUST treat network errors, timeouts,
429and every5xxas temporary: retry with exponential backoff and jitter (for example 30 s, 1 min, 2 min, 4 min, then every 10–15 min). When a response carriesRetry-After, wait at least that long. - MUST NOT stop polling because of an error, however many in a row. Keep retrying until the service answers, then continue from your last successful cursor. During a past outage, an integration that gave up after a single
500stopped receiving data entirely. - MUST advance the sync cursor only after a response has been received in full and stored. A failed poll then costs nothing: the next one fetches the same data.
- MUST alert your own operations staff when no poll has succeeded for a set time (we suggest three polling intervals, for example 30 minutes). An empty response is a success. We cannot see that your side has stopped polling.
- SHOULD log and alert on any other
4xxinstead of retrying it unchanged. It points to a bug in the request or a configuration problem.
- MUST poll incrementally by server-update time, not by measurement time, every 5–15 minutes (see Data Synchronization). Devices that were offline upload old readings late, and a reading's validity can change after it was first sent; both appear only by server-update time.
- SHOULD take the next cursor from the time the request was sent, minus an overlap of about a minute, rather than from when processing finished. Records updated while your poll runs, or hidden by clock differences, are then fetched again rather than missed; requirement 12 makes the repeats harmless.
- SHOULD run the initial backfill by measurement time, at most 30 days back, in bounded chunks (day by day, or per ward or device), then switch to incremental polling. Responses are not paginated: one unbounded query returns everything at once.
- MUST store records keyed by their stable identifier (see Data Synchronization) and replace the stored copy when a record arrives again, rather than adding a second one. Status changes and corrections arrive as the same record with new content.
- SHOULD periodically (for example hourly) re-fetch the diuresis of a recent window by measurement time, such as the last 24 hours, and replace your stored diuresis for that window with the response. Invalidating a measurement replaces the diuresis records that used it with one recalculated record; incremental polling delivers that record but never reports a removal.
- MUST check every measurement's validity status, and exclude invalid measurements from clinical calculations or show them distinctly (see Measurement Validity). Anomaly detection, device hardware checks and clinical staff all flag readings, and flagged readings are still returned.
- MUST expect validity to change after a measurement was first delivered, and update the stored copy (requirement 12).
- SHOULD use our pre-calculated diuresis rather than recomputing it from measurements. Our calculation applies the A-NA (Anomaly–Non Anomaly) rule during anomaly runs, which cannot be reproduced from the measurements alone; your figures would differ during those periods.
- MUST NOT treat a diuresis value as a rate. It is the urine volume in mL accumulated over its interval; a rate needs the interval's length.
- MUST own the mapping from device to patient or bed. Urogram data is device-centric and carries no patient identity of its own. Key the mapping on the device serial number printed on the device label, and update it when a device moves, or new data keeps arriving under the old patient or bed. See Device-to-Patient Pairing.
- SHOULD monitor service health with the public readiness check
GET https://www.app.htgmedical.com/web/health/ready/(200 = ready, 503 = a backend dependency failed) together with the API-specific check under Health Monitoring. A healthy service does not prove your data is fresh, so requirement 7 still applies.
- SHOULD pin to the API's own version tag (
fhir-v…orhl7v2-v…), not the application version, and read the Changelog before upgrading. We announce breaking changes before they ship. - SHOULD validate the integration against the Test environment before production, and contact us through Support about credentials, outages or unexpected data.
| If your system... | Use |
|---|---|
| Wants the IHE PCD-01 message shape | PCD-01 |
| Uses IEEE 11073 MDC codes | PCD-01 |
| Is MetaVision, IntelliVue, or similar CIS | Generic ORU |
| Needs simple string-based OBX identifiers | Generic ORU |
| Routes messages to a bed by device label (PV1-3) | Generic ORU |
HL7 version: 2.6 Coding system: IEEE 11073 MDC (designator MDC) IHE Profile OID: 1.3.6.1.4.1.19376.1.6.1.1.1
"Aligned", not "compliant". This profile emits the PCD-01 ORU^R01 message shape with IEEE 11073 MDC nomenclature, and identifies itself with the PCD-01 profile OID in MSH-21. It is not a conformant IHE PCD-01 implementation: PCD-01 is a push transaction over MLLP or WS*, and additionally requires an OBX-4 containment tree with MDS/VMD/Channel device segments. Neither is present here. If you need certified PCD-01 conformance, talk to us before you design against this — see Transport.
A single message covers every device in the result set.
MSH (v2.6, ORU^R01^ORU_R01, IHE OID in MSH-21) PID (patient_id param or "UNKNOWN" placeholder; PID-5 = unknown-name form) PV1 (patient class "U") [Per device, volume measurements]: OBR (OBR-4 = MDC_VOL_URINE_COL) OBX* (measurements: MDC_VOL_URINE_COL, device serial in OBX-18) [Per device, diuresis]: OBR (OBR-4 = MDC_VOL_URINE_BAL_PD) OBX* (diuresis: MDC_VOL_URINE_BAL_PD, device serial in OBX-18)
Note on segment grouping: Volume and diuresis observations are not mixed inside one OBR group. All volume groups (one OBR per device) are emitted first, followed by all diuresis groups (again one OBR per device). OBR-1 and OBX-1 set IDs increment continuously across the whole message — OBX-1 does not restart at 1 inside each OBR group.
| Observation | OBX-3 Code | OBX-3 Display | OBX-3 System | OBX-6 Units |
|---|---|---|---|---|
| Urine volume collected | 157744 | MDC_VOL_URINE_COL | MDC | 263762^MDC_DIM_MILLI_L (mL) |
| Urine output over the diuresis period | 157732 | MDC_VOL_URINE_BAL_PD | MDC | 263762^MDC_DIM_MILLI_L (mL) |
MDC codes are full 32-bit context-free codes — (partition << 16) | term, with metrics in partition 2 and units in partition 4 — and the OBX-3/OBX-6 coding-system component is the HL7 v2 designator MDC. (urn:iso:std:iso:11073:10101 is the FHIR system URI for the same code system; it is not valid inside a v2 message.) Every code above was verified against the IEEE 11073-10101 code system.
Both observations are volumes in mL. A diuresis record is the urine volume accumulated between two consecutive valid measurements, so it is reported as a volume over its stored interval, not as a flow rate. OBX-14 carries the interval start only; this profile does not expose the interval end or duration. Do not infer mL/h from this timestamp alone. Obtain the full interval through FHIR or REST.
One device (serial number 908W) with one volume measurement and one diuresis record:
MSH|^~\&|UROGRAM|Example Hospital|||20260315143000+0000||ORU^R01^ORU_R01|a1b2c3d4e5f6a7b8c9d0|P|2.6|||||||||IHE_PCD_001^IHE PCD^1.3.6.1.4.1.19376.1.6.1.1.1^ISO PID|||UNKNOWN^^^UROGRAM||^^^^^^U||||||||||||||||||||||||||Y PV1||U OBR|1|||157744^MDC_VOL_URINE_COL^MDC OBX|1|NM|157744^MDC_VOL_URINE_COL^MDC||150|263762^MDC_DIM_MILLI_L^MDC|||||F|||20260315143000+0000||||908W OBR|2|||157732^MDC_VOL_URINE_BAL_PD^MDC OBX|2|NM|157732^MDC_VOL_URINE_BAL_PD^MDC||63|263762^MDC_DIM_MILLI_L^MDC|||||F|||20260315133000+0000||||908W
Notes on the segments above:
- OBX-18 (Equipment Instance Identifier) carries the device serial number.
- OBR-4 (Universal Service Identifier) repeats the group's OBX-3 code, so an OBR group is self-describing.
- PID-5 uses the HL7 "no name available" form — empty name components with name type code
U(unspecified). PID-31 isYwhen nopatient_idwas supplied, so you can distinguish "identity not known" from "identity not sent". - MSH-15/MSH-16 (accept/application acknowledgement) and MSH-18 (character set) are not populated on this profile; the message is UTF-8 encoded.
HL7 version: 2.3 Coding system: String identifiers (UROGRAM namespace)
This profile is designed for CIS systems like MetaVision that assign incoming data to a virtual bed location and use simple string-based OBX identifiers.
Consuming systems route on PV1-3 (Assigned Patient Location), which carries the Urogram device serial number. A message therefore describes exactly one device.
A response covering N devices contains N ORU^R01 messages, concatenated in the response body. Each message begins with its own MSH segment, so consumers split the body on MSH| — the same framing integration engines (Mirth Connect / Open Integration Engine, Rhapsody) already apply to an HL7 batch.
This assumes a device stays at one bed. PV1-3 identifies the device, and your CIS maps that string to a bed. If a Urogram is physically moved to another bed, the mapping must be updated on your side — otherwise data continues to land at the old location. Request a single device per call with
device_serial_numberif you prefer to manage the mapping request-side.
Per device, one message:
MSH (v2.3, MSH-15=AL, MSH-16=NE) PID (no patient identity — PID-5 uses the "unknown name" form) PV1 (patient class "I", PV1-3 = device serial number → bed location) [Volume measurements]: OBR (OBR-4 = "UrineVolume", OBR-7 = timestamp, OBR-10 = device serial number) OBX* (measurements: "UrineVolume") [Diuresis]: OBR (OBR-4 = "UrineOutput", OBR-7 = timestamp, OBR-10 = device serial number) OBX* (diuresis: "UrineOutput")
Note on segment grouping: volume and diuresis observations occupy separate OBR groups within the device's message — volume first, then diuresis. OBX-1 set IDs increment across the message rather than restarting inside each OBR group.
Note on OBR-10:
OBR-10also carries the device serial number, and remains populated for consumers that read it. PV1-3 is the routing key.
| Observation | OBX-3 Identifier | Description |
|---|---|---|
| Urine volume collected | UrineVolume | Urine volume in mL |
| Urine output over the diuresis period | UrineOutput | Urine volume in mL accumulated over the period |
Both are volumes in mL.
UrineOutputis the volume accumulated between two consecutive valid measurements, not a rate. OBX-14 carries the interval start only; this profile does not expose the end or duration. Obtain the full interval through FHIR or REST before calculating mL/h.
The existing Generic profile leaves OBX-6 empty; its numeric unit is mL by this contract. Adding explicit units requires receiver compatibility testing and an opt-in/versioned profile. Both profiles expose urine volume OBX-5 values as whole mL without a decimal suffix, rounded to nearest with ties away from zero. OBX-2 remains NM; codes and units remain unchanged. Measurement grams are subtracted before conversion using 1.015 g/mL and then rounded; diuresis is rounded from the stored mL value without converting again. Stored values and internal calculations do not change. Summing rounded observations can differ from rounding their unrounded total. Historical diuresis records require a deployment-specific unit audit; this release does not rewrite them or apply another conversion on export.
Two devices (908W with a volume measurement and a diuresis record, 95CK with a volume measurement only) — so the response body holds two messages:
MSH|^~\&|UROGRAM|Example Hospital|||20260315143000+0000||ORU^R01|a1b2c3d4e5f6a7b8c9d0|P|2.3|||AL|NE PID|||UNKNOWN^^^UROGRAM||^^^^^^U PV1||I|908W OBR|1|||UrineVolume^UrineVolume^UROGRAM|||20260315143000.000+0000|||908W|||||||||||||||||^^^^^R OBX|1|NM|UrineVolume^UrineVolume^UROGRAM||150||||||F|||20260315143000+0000 OBR|2|||UrineOutput^UrineOutput^UROGRAM|||20260315133000.000+0000|||908W|||||||||||||||||^^^^^R OBX|2|NM|UrineOutput^UrineOutput^UROGRAM||63||||||F|||20260315133000+0000 MSH|^~\&|UROGRAM|Example Hospital|||20260315143000+0000||ORU^R01|b2c3d4e5f6a7b8c9d0e1|P|2.3|||AL|NE PID|||UNKNOWN^^^UROGRAM||^^^^^^U PV1||I|95CK OBR|1|||UrineVolume^UrineVolume^UROGRAM|||20260315143000.000+0000|||95CK|||||||||||||||||^^^^^R OBX|1|NM|UrineVolume^UrineVolume^UROGRAM||197||||||F|||20260315143000+0000
Notes on the segments above:
- OBX-18 is not populated on this profile: the Equipment Instance Identifier field was introduced in HL7 v2.5, so on v2.3 the device is identified by PV1-3 and OBR-10.
- OBR-27 (Quantity/Timing) carries the minimal routine-priority value
^^^^^R. It is a required field up to v2.3; it is absent from the v2.6 PCD-01 profile, where timing moved to the TQ1 segment. - PID-31 is not set here — the field was introduced in HL7 v2.4.
All emitted timestamps are UTC and carry an explicit +0000 offset, so no timezone has to be inferred:
| Field | Format | Example |
|---|---|---|
MSH-7 | YYYYMMDDHHMMSS+0000 | 20260315143000+0000 |
OBR-7 | YYYYMMDDHHMMSS.SSS+0000 | 20260315143000.000+0000 |
OBX-14 | YYYYMMDDHHMMSS+0000 | 20260315143000+0000 |
Convert to your local zone on receipt. Note that Czech local time is UTC+1 in winter and UTC+2 under daylight saving, so a fixed one-hour adjustment is not correct year-round — use the offset.
How Urogram data maps to HL7v2 segments:
| Urogram Data | PCD-01 Location | Generic Location | Notes |
|---|---|---|---|
| Hospital name | MSH-4 (Sending Facility) | MSH-4 | Same in both profiles |
| Device serial number | OBX-18 | PV1-3 (routing key), OBR-10 | Preferred integration identifier. On the Generic profile PV1-3 is the virtual bed routing key, and there is one message per device |
| Device ID (internal) | Filter only | — | Internal devEUI-backed identifier retained for backward-compatible filtering |
| Urine volume (mL) | OBX-5 (MDC 157744) | OBX-5 ("UrineVolume") | Computed: (raw_g - tare_g) / 1.015 |
| Measurement timestamp | OBX-14 | OBX-14 | When the device took the reading |
| Measurement status | OBX-11 | OBX-11 | Mapped via status table below |
| Invalid/inactive measurement explanation | NTE-3 after its OBX | NTE-3 after its OBX | English dashboard clinical label |
| Urine output over the period (mL) | OBX-5 (MDC 157732) | OBX-5 ("UrineOutput") | Server-calculated volume, not a rate. Computed: (raw_g at period end - raw_g at period start) / 1.015 |
| Diuresis period start | OBX-14 | OBX-14 | Start of calculation period |
The API returns all measurements, including those flagged as invalid (requirements 14–15). An invalid measurement has OBX-11 = "X" (Cannot obtain), mapped from the internal entered-in-error status; any other OBX-11 value is valid.
Hardware-invalid readings and inactive heartbeats are included with OBX-11 X. Both profiles can emit one optional NTE immediately after a measurement's OBX, with the same English clinical label used by the dashboard and FHIR: Inactive (No clip detected), Invalid (Tilt exceeds the reading limit), Manually hidden, or Flagged as anomaly. The grammar is OBX → optional NTE for each measurement; valid measurements and calculated diuresis do not receive diagnostic NTE segments.
NTE-1 is 1 within each observation, NTE-2 is O (other source), and NTE-3 contains the label. Reasons are ordered and deduplicated, unknown codes appear as Unspecified reason, and admin-only forced inactivity details are omitted. An Inactive note describes measurement mode at the heartbeat time while the device is communicating. Failed measurement attempts are not observations. Legacy heartbeat markers contain protocol flags instead of weights, so their OBX-5 is empty and OBX-11 is X.
OBX|1|NM|UrineVolume^UrineVolume^UROGRAM||0||||||X|||20260315143000+0000 NTE|1|O|Inactive (No clip detected)
The diuresis observations (MDC_VOL_URINE_BAL_PD / UrineOutput) are calculated from valid measurements only, so if you display those, as requirement 16 recommends, no further filtering is needed. A measurement that becomes invalid is delivered again by updated_since with OBX-11 = X; update the stored observation by the key described under Data Synchronization.
Both ORU endpoints (/v1/pcd01/ORU/ and /v1/generic/ORU/) accept the same query parameters:
| Parameter | Required | Format | Description |
|---|---|---|---|
date_from | Yes | ISO 8601 datetime | Start of the clinical date range (inclusive). Filters by when the device took the measurement (OBX-14). |
date_to | Yes | ISO 8601 datetime | End of the clinical date range (inclusive). |
ward_id | No | UUID | Filter results to a specific hospital ward. Obtain ward UUIDs from GET /v1/wards/. If omitted, results include data from all wards in your hospital. |
device_serial_number | No | String | Filter results to a specific device by serial number (the ID printed on the device label, e.g. 908W). Preferred for integrations. |
device_id | No | String | Filter results to a specific device by internal devEUI-backed ID (e.g. 0004A30B00E1F2C3). Retained for backward compatibility. |
include_diuresis | No | Boolean | Whether to include diuresis (urine output volume) observations in the response. Default: true. Only the literal value false (case-insensitive) disables them; any other value is treated as true. Leave it on: requirement 16 recommends displaying these server-calculated values rather than recomputing them. |
updated_since | No | ISO 8601 datetime | Only return records where the server's internal updated_at timestamp is strictly after this value. Use this for incremental sync — see Data Synchronization. |
patient_id | No | String | PCD-01 only. Patient identifier to include in the PID-3 segment. If omitted, PID-3 contains the placeholder UNKNOWN. Not available on the Generic ORU endpoint. |
Note on the date window and diuresis:
date_from/date_toare applied differently to the two observation types. A volume measurement matches when its measurement time falls inside the window. A diuresis record matches only when its whole calculation period is contained in the window (period start ≥date_fromand period end ≤date_to). Keep the window comfortably wider than the interval you actually need, or a diuresis record straddling the boundary will be omitted.
Example — all parameters:
GET /hl7v2/v1/pcd01/ORU/?date_from=2026-03-01T00:00:00Z&date_to=2026-03-02T00:00:00Z&ward_id=550e8400-e29b-41d4-a716-446655440000&device_serial_number=908W&include_diuresis=false&updated_since=2026-03-01T12:00:00Z&patient_id=PAT-12345 HTTP/1.1
Example — minimum (required only):
GET /hl7v2/v1/generic/ORU/?date_from=2026-03-01T00:00:00Z&date_to=2026-03-02T00:00:00Z HTTP/1.1
By default, ORU endpoints return data from all wards in your hospital. You can optionally filter by a specific ward using the ward_id query parameter.
GET /hl7v2/v1/wards/ HTTP/1.1
Authorization: Bearer {token}
Response:
{
"hospital_id": "12345678-abcd-ef01-2345-6789abcdef01",
"hospital_name": "General Hospital",
"wards": [
{ "id": "550e8400-e29b-41d4-a716-446655440000", "name": "ICU Ward A" },
{ "id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8", "name": "General Ward B" }
]
}
Pass the ward id as the ward_id parameter on any ORU endpoint:
GET /hl7v2/v1/pcd01/ORU/?date_from=2026-03-01T00:00:00Z&date_to=2026-03-02T00:00:00Z&ward_id=550e8400-e29b-41d4-a716-446655440000 GET /hl7v2/v1/generic/ORU/?date_from=2026-03-01T00:00:00Z&date_to=2026-03-02T00:00:00Z&ward_id=550e8400-e29b-41d4-a716-446655440000
The ward_id parameter can be combined with all other query parameters (device_serial_number, device_id, include_diuresis, updated_since).
- Get credentials — Receive
client_idandclient_secretfrom HTG - Obtain token —
POST /oauth/v1/token/withscope=m2m.read - Discover wards —
GET /hl7v2/v1/wards/to see available ward IDs and names - Check capabilities —
GET /hl7v2/v1/{profile}/metadata/to verify supported observations - Pull data —
GET /hl7v2/v1/{profile}/ORU/?date_from=...&date_to=...with optionalward_id,device_serial_number,device_id,include_diuresis, andupdated_sinceparameters
Each observation has two important timestamps:
| Timestamp | HL7 Field | Query Parameter | Description |
|---|---|---|---|
| Measurement time | OBX-14 | date_from / date_to | When the device physically took the reading |
| Server receipt time | (internal) | updated_since | When the data was received or last modified by the server |
Server-update time (requirement 9) is the updated_since parameter. It is not emitted in the message, so keep your own cursor. A poll on date_from alone misses readings that an offline device uploads late with an old OBX-14, and misses validity changes.
1. poll_started = now - 1 minute (requirement 10: overlap)
2. GET /hl7v2/v1/{profile}/ORU/?date_from=<30 days ago>&date_to=<now>
&updated_since=<cursor>
3. On timeout, 429 or 5xx: back off and retry from step 1 with the
SAME cursor; never give up (requirements 4-6)
4. Split the body on MSH|, upsert each OBX by its key (below)
5. Only after step 4 succeeded: cursor = poll_started
6. Wait 5-15 minutes, repeat
Keep date_from wide (30 days) during steady-state polling: updated_since narrows the date-bounded result, so a late upload is only returned if its OBX-14 is inside the window.
HL7v2 messages carry no per-observation identifier: MSH-10 is new on every response, and OBR-1/OBX-1 are positions within the message. Key each stored observation (requirement 12) on device, observation type and OBX-14:
| Profile | Device | Observation type | Time |
|---|---|---|---|
| PCD-01 | OBX-18 | OBX-3 code (157744 volume, 157732 diuresis) | OBX-14 |
| Generic ORU | PV1-3 | OBX-3 (UrineVolume, UrineOutput) | OBX-14 |
OBX-14 is the measurement time for a volume and the period start for a diuresis. A re-delivered measurement (for example newly OBX-11 = X) has the same key, so replace the stored copy. This is the best key the messages allow, not a guaranteed one: OBX-14 has whole-second precision, so two readings from one device within the same second would share it.
When a measurement is invalidated, the diuresis records before and after it are usually removed and, where the valid neighbours allow, replaced by one record spanning them. That record starts where the first removed one did, so it overwrites it under this key; a removed record that nothing replaces is never re-sent and stays in your store until you reconcile. For the diuresis reconciliation in requirement 13, re-query with date_from/date_to around the recent window and replace the stored diuresis whose period lies wholly inside that window: only those are returned (see the note under Query Parameters).
Use this to size your first sync and your storage. There is no pagination — a query returns every matching observation in one response — so the numbers matter.
Observation count per device per day:
measurements = 86400 / measurement_interval_seconds
observations = measurements x 2 (roughly, when include_diuresis=true:
one diuresis per consecutive valid pair)
The measurement interval is configured per device, so ask us for the value used in your deployment rather than assuming one. Diuresis records are only created between measurements no more than 62 minutes apart, so sparse or interrupted data yields proportionally fewer of them.
Measured payload sizes (UTF-8 bytes on the wire):
| Profile | Per observation | Per-device message overhead |
|---|---|---|
| Generic ORU | ~80 B | ~240 B (MSH + PID + PV1) plus ~90 B per OBR group |
| PCD-01 | ~115 B | one MSH + PID + PV1 for the whole response |
PCD-01 is larger per observation because the MDC codes and units are long coded triplets rather than short strings.
Worked example — 20 devices on a 5-minute interval, diuresis included:
288 measurements + ~288 diuresis = ~576 observations/device/day x 20 devices = ~11,500 observations/day Generic: ~11,500 x 80 B = ~0.9 MB/day 30-day backfill in one response = ~28 MB
A 30-day backfill across every ward is therefore a tens-of-megabytes single response. Bound your initial sync (requirement 11): loop day by day over date_from/date_to without updated_since, or filter with ward_id / device_serial_number. Then switch to updated_since for steady-state polling, where each response is normally small or empty.
Requirement 18 applies: your system maps the device serial number to a patient or bed.
For Generic ORU consumers (MetaVision, etc.):
PV1-3(also inOBR-10) contains the device's serial number (e.g.,908W)- Configure your CIS to map this string to a virtual bed location
- The patient assigned to that bed receives the data
For PCD-01 consumers:
- Pass the
patient_idquery parameter if your system knows the patient - Without
patient_id, the PID segment contains a placeholder (UNKNOWN)
When no observations match your query, both ORU endpoints return 200 OK with an empty response body — not a message with zero observations.
This is deliberate: an ORU^R01 with no OBR group does not satisfy the HL7 v2 message grammar, so sending nothing is preferable to sending something a conformance check would reject. It is also the common case in steady-state polling, where most updated_since requests legitimately have nothing new to report.
Handle a zero-length body as "no new data" and skip the parse:
body = response.text
if not body.strip():
return [] # nothing new since the last cursor
messages = [m for m in body.split("MSH|") if m.strip()]
| HTTP Status | Meaning | Action |
|---|---|---|
| 401 Unauthorized | Token missing, invalid, or expired | Request one new access token and retry once (requirement 2) |
| 403 Forbidden | Token valid but no hospital linked, or missing m2m.read scope | Do not retry in a loop; contact HTG to link your application to a hospital (requirement 3) |
Returned as JSON with an error field:
{
"error": "Both date_from and date_to query parameters are required."
}
| HTTP Status | Cause |
|---|---|
| 400 Bad Request | Missing date_from or date_to |
| 400 Bad Request | Invalid date format (must be ISO 8601) |
| 400 Bad Request | Invalid ward_id (must be a valid UUID) |
| 400 Bad Request | Invalid updated_since format (must be ISO 8601) |
{
"error": "HL7v2 message conversion failed.",
"profile": "PCD-01"
}
| HTTP Status | Meaning | Action |
|---|---|---|
| 500 Internal Server Error | HL7v2 message conversion failed | Retry with backoff and keep polling (requirements 4–5); if it persists, contact HTG |
| 429 Too Many Requests | Rate limit reached | Wait for Retry-After, see Rate Limits |
| Other 5xx, timeout, network error | Service or network temporarily unavailable; the body may not be JSON | Retry with backoff and keep polling (requirements 4–5) |
The limit in requirement 20 applies to every HL7v2 endpoint, including the unauthenticated /v1/{profile}/metadata/ endpoints and /v1/wards/.
A throttled request returns 429 Too Many Requests with the JSON body {"detail": "Request was throttled. Please try again later."} and the headers Retry-After: 60, X-RateLimit-Limit and X-RateLimit-Remaining: 0.
Requirement 19 pairs the public readiness check with an API-specific check. The HL7v2 API has no dedicated health endpoint; use a small real request from your own network, with your own credentials:
curl --silent --show-error --fail --max-time 30 -o /dev/null \ -H "Authorization: Bearer $TOKEN" \ "https://app.htgmedical.com/hl7v2/v1/generic/ORU/?date_from=$FROM&date_to=$TO&device_serial_number=908W"
A 200 (with a body, or empty when nothing matched) shows that authentication, your hospital link, the database query and message conversion all work. Keep the window and filter narrow, and keep message bodies out of monitoring logs. The /v1/{profile}/metadata/ endpoints return a fixed description and only prove routing: they check neither your token nor the database. Neither check shows that new data is arriving; that is the alert in requirement 7.
An empty response body means "no observations matched" — it is a valid answer, not an error, and it is the normal steady state when polling with updated_since. If you expected data, work down this list:
| # | Cause | How to tell | Fix |
|---|---|---|---|
| 1 | Nothing new since your cursor | Normal during incremental polling | None — this is expected. updated_since is strictly greater-than, so a record exactly on the cursor is excluded |
| 2 | The date window misses the readings | Widen date_from/date_to and retry | Filter on measurement time (OBX-14), not on when you polled |
| 3 | A diuresis straddles the window edge | Volume measurements appear but diuresis does not | A diuresis matches only when its whole period is inside the window. Widen it — see Query Parameters |
| 4 | ward_id does not match | Retry without ward_id | Confirm the UUID against GET /v1/wards/; a device may have been moved to another ward |
| 5 | The device is not in your hospital | Retry without device_serial_number / device_id | All queries are scoped to the hospital linked to your OAuth2 application. A device assigned to another hospital, or to none, returns nothing |
| 6 | device_serial_number does not match exactly | Compare against the serial in a broader response | The match is exact and case-sensitive — no partial or trimmed matching |
| 7 | include_diuresis disabled it | You passed include_diuresis=false | Only the literal value false disables diuresis; any other value leaves it enabled |
Not a cause: invalid measurements are still returned, flagged with OBX-11 = "X". They are never silently dropped, so anomaly detection cannot empty your response.
Your token is valid but the OAuth2 application behind it is not linked to a hospital, or the m2m.read scope was not requested. Contact us to link the application — see Support.
This API is versioned independently of the Urogram application. Releases are tagged hl7v2-v<major>.<minor>.<patch>, and the version in this document (info.version) tracks that stream.
| Change | Version bump | What you should expect |
|---|---|---|
| New optional query parameter, new segment or field populated | Minor | Backward compatible; existing parsers keep working |
| Changed code, renamed OBX-3 identifier, changed unit or message framing | Flagged as breaking | We will contact you before such a change ships — see the note below |
| Documentation-only correction | Patch | No behaviour change |
Pin to the API tag, not the application version. Application releases (v*) do not imply an API change, and API releases do not follow the application's version numbers. If you need to know whether a deployment carries a given API version, ask us — the Changelog table at the end of this document is the authoritative history.
Breaking changes are announced before deployment. If a change would require work on your side, we will contact you rather than expect you to notice it here.
For technical support or to request API credentials:
- Email: max@htgmedical.com
- Phone: +420 774 123 097
| Version | Date | Changes |
|---|---|---|
| 1.4.1 | 2026-10 | Documentation: new shared Integration Requirements section (token reuse, retry with backoff and never stop polling, stale-sync alerting, cursor overlap, upsert keys, diuresis reconciliation, clinical use), kept identical in the FHIR and HL7v2 specs. Added a token caching example, how to key stored observations, a health check and server-error guidance. Message formats are unchanged. |
| 1.4 | 2026-10 | Urine-volume and diuresis OBX-5 values now always round to whole mL, nearest with ties away from zero. Both profiles may emit one observation-level NTE after an invalid or inactive measurement OBX. Hardware-invalid readings and valueless diuresis use OBX-11 X; legacy heartbeat markers have an empty OBX-5. Clarified that OBX-14 is the interval start only. OBX-2 remains NM; codes, units and stored values are unchanged. |
| 1.3 | 2026-07 | Reworked profile. OBX-3/OBX-6 carry IEEE 11073 MDC codes as full 32-bit values with the MDC designator (157744^MDC_VOL_URINE_COL^MDC, 157732^MDC_VOL_URINE_BAL_PD^MDC, 263762^MDC_DIM_MILLI_L^MDC). Diuresis is reported as a volume in mL over its period, identified as UrineOutput on the Generic profile. The Generic profile emits one message per device, routed on PV1-3. All timestamps carry an explicit +0000 UTC offset. Messages conform to the HL7 v2 ORU^R01 grammar (MSH-9 message structure on v2.6, PID-5/PID-31, a PID segment on the Generic profile, OBR-4, OBR-27). An empty result returns an empty body. The v2.6 profile is described as IHE PCD-01 aligned. |
| 1.2 | 2026-05 | Consolidated device serial identity: ORU messages use serial_number for device routing and added preferred device_serial_number filtering. |
| 1.1 | 2026-03 | Added updated_since parameter for sync-safe incremental polling. Hidden/invalid measurements now returned with OBX-11="X" status instead of being silently excluded. Added measurement validity and data synchronization documentation. |
| 1.0 | 2026-03 | Initial HL7v2 API release with PCD-01 and Generic ORU profiles, ward discovery endpoint |