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.

Overview

The Urogram HL7v2 API provides urine volume measurements and diuresis calculations as standard HL7 v2 ORU^R01 messages over HTTP. Two profiles are available:

ProfileEndpointHL7 VersionUse Case
IHE PCD-01 aligned/hl7v2/v1/pcd01/ORU/v2.6PCD-01 message shape with IEEE 11073 MDC nomenclature
Generic ORU/hl7v2/v1/generic/ORU/v2.3General CIS integration (MetaVision, IntelliVue, Dräger, etc.)

All data is automatically scoped to your hospital based on your API credentials.

Transport: Pull over HTTP Today, MLLP Push on Request

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.

Base URL

EnvironmentURLNotes
Productionhttps://app.htgmedical.com/hl7v2/Live patient data
Testhttps://staging.app.htgmedical.com/hl7v2/For integration testing — requires a test tenant, see below
Local developmenthttp://localhost:8000/hl7v2/Only if you are running Urogram yourself

Getting a Test Environment

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.

Quickstart

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

  1. GET /hl7v2/v1/wards/ — discover ward UUIDs if you want to filter by ward
  2. Swap generic for pcd01 in the path to see the MDC-coded variant
  3. Add updated_since and switch to incremental polling — see Data Synchronization. Do this before you go live; a date_from-only poll silently misses late-arriving data.

Authentication

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.

Credentials

You will receive from HTG:

  • client_id — Your application identifier
  • client_secret — Your secret key (keep this secure!)

Step 1: Request an Access Token

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"
}

Step 2: Use the Access Token

Include the token in the Authorization header for all API requests:

GET /hl7v2/v1/generic/ORU/?date_from=2026-01-01T00:00:00Z&date_to=2026-01-02T00:00:00Z HTTP/1.1
Host: app.htgmedical.com
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9...

Token Reuse

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

Integration Requirements

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.

Credentials and tokens

  1. MUST keep the client_secret in a secret store, never in source control, logs or URLs. It grants read access to your hospital's data.
  2. MUST cache the access token and reuse it for every request until shortly before expires_in elapses (for example, renew 5 minutes early). On a 401, 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.
  3. MUST NOT retry a 403 in a loop. It means the token lacks the m2m.read scope or the application is not linked to a hospital, which only a configuration change fixes; contact us.

Errors, retries and staying alive

  1. MUST treat network errors, timeouts, 429 and every 5xx as 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 carries Retry-After, wait at least that long.
  2. 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 500 stopped receiving data entirely.
  3. 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.
  4. 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.
  5. SHOULD log and alert on any other 4xx instead of retrying it unchanged. It points to a bug in the request or a configuration problem.

Synchronisation

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.

Validity

  1. 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.
  2. MUST expect validity to change after a measurement was first delivered, and update the stored copy (requirement 12).

Diuresis

  1. 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.
  2. 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.

Devices and patients

  1. 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.

Health monitoring

  1. 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.

Rate limits

  1. MUST stay within 100 requests per minute per source IP on the data endpoints, and on 429 wait for Retry-After (60 seconds). Monitors and pollers sharing an IP share the limit. Endpoint-specific details are under Rate Limits. The 5–15 minute polling interval sits far below the limit.

Versioning and support

  1. SHOULD pin to the API's own version tag (fhir-v… or hl7v2-v…), not the application version, and read the Changelog before upgrading. We announce breaking changes before they ship.
  2. SHOULD validate the integration against the Test environment before production, and contact us through Support about credentials, outages or unexpected data.

Clinical use

  1. The data provided through this API is intended to support clinical decision-making but MUST NOT be the sole basis for clinical actions. Always correlate it with other clinical observations and exercise professional judgment.

Choosing a Profile

If your system...Use
Wants the IHE PCD-01 message shapePCD-01
Uses IEEE 11073 MDC codesPCD-01
Is MetaVision, IntelliVue, or similar CISGeneric ORU
Needs simple string-based OBX identifiersGeneric ORU
Routes messages to a bed by device label (PV1-3)Generic ORU

IHE PCD-01 Aligned Profile

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.

Message Structure

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.

Supported Observations

ObservationOBX-3 CodeOBX-3 DisplayOBX-3 SystemOBX-6 Units
Urine volume collected157744MDC_VOL_URINE_COLMDC263762^MDC_DIM_MILLI_L (mL)
Urine output over the diuresis period157732MDC_VOL_URINE_BAL_PDMDC263762^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.

Example Message

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 is Y when no patient_id was 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.

Generic ORU Profile

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.

One Message Per Device

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_number if you prefer to manage the mapping request-side.

Message Structure

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-10 also carries the device serial number, and remains populated for consumers that read it. PV1-3 is the routing key.

Supported Observations

ObservationOBX-3 IdentifierDescription
Urine volume collectedUrineVolumeUrine volume in mL
Urine output over the diuresis periodUrineOutputUrine volume in mL accumulated over the period

Both are volumes in mL. UrineOutput is 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.

Example Message

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.

Timestamps

All emitted timestamps are UTC and carry an explicit +0000 offset, so no timezone has to be inferred:

FieldFormatExample
MSH-7YYYYMMDDHHMMSS+000020260315143000+0000
OBR-7YYYYMMDDHHMMSS.SSS+000020260315143000.000+0000
OBX-14YYYYMMDDHHMMSS+000020260315143000+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.

Field Mapping Reference

How Urogram data maps to HL7v2 segments:

Urogram DataPCD-01 LocationGeneric LocationNotes
Hospital nameMSH-4 (Sending Facility)MSH-4Same in both profiles
Device serial numberOBX-18PV1-3 (routing key), OBR-10Preferred 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 timestampOBX-14OBX-14When the device took the reading
Measurement statusOBX-11OBX-11Mapped via status table below
Invalid/inactive measurement explanationNTE-3 after its OBXNTE-3 after its OBXEnglish 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 startOBX-14OBX-14Start of calculation period

OBX-11 Status Mapping

Urogram StatusOBX-11 CodeMeaning
finalFFinal result
preliminaryPPreliminary
amended / correctedCCorrected
registeredIIncomplete
cancelled / entered-in-errorXCannot obtain
unknownPPreliminary (default)

Measurement Validity

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.

Query Parameters

Both ORU endpoints (/v1/pcd01/ORU/ and /v1/generic/ORU/) accept the same query parameters:

ParameterRequiredFormatDescription
date_fromYesISO 8601 datetimeStart of the clinical date range (inclusive). Filters by when the device took the measurement (OBX-14).
date_toYesISO 8601 datetimeEnd of the clinical date range (inclusive).
ward_idNoUUIDFilter 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_numberNoStringFilter results to a specific device by serial number (the ID printed on the device label, e.g. 908W). Preferred for integrations.
device_idNoStringFilter results to a specific device by internal devEUI-backed ID (e.g. 0004A30B00E1F2C3). Retained for backward compatibility.
include_diuresisNoBooleanWhether 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_sinceNoISO 8601 datetimeOnly return records where the server's internal updated_at timestamp is strictly after this value. Use this for incremental sync — see Data Synchronization.
patient_idNoStringPCD-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_to are 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_from and 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

Ward Filtering (Optional)

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.

Step 1: Discover Available Wards

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" }
  ]
}

Step 2: Filter by Ward

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).

Integration Workflow

Recommended Setup Steps

  1. Get credentials — Receive client_id and client_secret from HTG
  2. Obtain token — POST /oauth/v1/token/ with scope=m2m.read
  3. Discover wards — GET /hl7v2/v1/wards/ to see available ward IDs and names
  4. Check capabilities — GET /hl7v2/v1/{profile}/metadata/ to verify supported observations
  5. Pull data — GET /hl7v2/v1/{profile}/ORU/?date_from=...&date_to=... with optional ward_id, device_serial_number, device_id, include_diuresis, and updated_since parameters

Data Synchronization

Understanding Timestamps

Each observation has two important timestamps:

TimestampHL7 FieldQuery ParameterDescription
Measurement timeOBX-14date_from / date_toWhen the device physically took the reading
Server receipt time(internal)updated_sinceWhen 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.

Recommended Polling Strategy

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.

Identifying a Stored Observation

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:

ProfileDeviceObservation typeTime
PCD-01OBX-18OBX-3 code (157744 volume, 157732 diuresis)OBX-14
Generic ORUPV1-3OBX-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).

Data Volumes and Sizing

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):

ProfilePer observationPer-device message overhead
Generic ORU~80 B~240 B (MSH + PID + PV1) plus ~90 B per OBR group
PCD-01~115 Bone 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.

Device-to-Patient Pairing

Requirement 18 applies: your system maps the device serial number to a patient or bed.

For Generic ORU consumers (MetaVision, etc.):

  • PV1-3 (also in OBR-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_id query parameter if your system knows the patient
  • Without patient_id, the PID segment contains a placeholder (UNKNOWN)

No Matching Data

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()]

Error Handling

Authentication Errors

HTTP StatusMeaningAction
401 UnauthorizedToken missing, invalid, or expiredRequest one new access token and retry once (requirement 2)
403 ForbiddenToken valid but no hospital linked, or missing m2m.read scopeDo not retry in a loop; contact HTG to link your application to a hospital (requirement 3)

Validation Errors

Returned as JSON with an error field:

{
  "error": "Both date_from and date_to query parameters are required."
}
HTTP StatusCause
400 Bad RequestMissing date_from or date_to
400 Bad RequestInvalid date format (must be ISO 8601)
400 Bad RequestInvalid ward_id (must be a valid UUID)
400 Bad RequestInvalid updated_since format (must be ISO 8601)

Server Errors

{
  "error": "HL7v2 message conversion failed.",
  "profile": "PCD-01"
}
HTTP StatusMeaningAction
500 Internal Server ErrorHL7v2 message conversion failedRetry with backoff and keep polling (requirements 4–5); if it persists, contact HTG
429 Too Many RequestsRate limit reachedWait for Retry-After, see Rate Limits
Other 5xx, timeout, network errorService or network temporarily unavailable; the body may not be JSONRetry with backoff and keep polling (requirements 4–5)

Rate Limits

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.

Health Monitoring

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.

Troubleshooting

I get an empty body (or a message with no OBX segments)

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:

#CauseHow to tellFix
1Nothing new since your cursorNormal during incremental pollingNone — this is expected. updated_since is strictly greater-than, so a record exactly on the cursor is excluded
2The date window misses the readingsWiden date_from/date_to and retryFilter on measurement time (OBX-14), not on when you polled
3A diuresis straddles the window edgeVolume measurements appear but diuresis does notA diuresis matches only when its whole period is inside the window. Widen it — see Query Parameters
4ward_id does not matchRetry without ward_idConfirm the UUID against GET /v1/wards/; a device may have been moved to another ward
5The device is not in your hospitalRetry without device_serial_number / device_idAll queries are scoped to the hospital linked to your OAuth2 application. A device assigned to another hospital, or to none, returns nothing
6device_serial_number does not match exactlyCompare against the serial in a broader responseThe match is exact and case-sensitive — no partial or trimmed matching
7include_diuresis disabled itYou passed include_diuresis=falseOnly 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.

I get a 403 rather than data

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.

Timestamps look an hour or two out

All emitted timestamps are UTC with an explicit +0000 offset. Parse the offset rather than assuming local time: Czech local time is UTC+1 in winter and UTC+2 under daylight saving, so a fixed adjustment is wrong for part of the year.

My parser rejects the message

Check that you are splitting a multi-message body correctly: the Generic profile returns one message per device, concatenated, each starting with MSH|. A parser that assumes a single message will see trailing garbage.

Versioning and Compatibility

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.

ChangeVersion bumpWhat you should expect
New optional query parameter, new segment or field populatedMinorBackward compatible; existing parsers keep working
Changed code, renamed OBX-3 identifier, changed unit or message framingFlagged as breakingWe will contact you before such a change ships — see the note below
Documentation-only correctionPatchNo 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.

Support

For technical support or to request API credentials:

  • Email: max@htgmedical.com
  • Phone: +420 774 123 097

Changelog

VersionDateChanges
1.4.12026-10Documentation: 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.42026-10Urine-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.32026-07Reworked 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.22026-05Consolidated device serial identity: ORU messages use serial_number for device routing and added preferred device_serial_number filtering.
1.12026-03Added 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.02026-03Initial HL7v2 API release with PCD-01 and Generic ORU profiles, ward discovery endpoint
Overview
HTG Medical
Servers
Production
https://app.htgmedical.com/hl7v2
Development
http://localhost:8000/hl7v2