GlucoseIQ

Export FHIR and Open mHealth

Build portable CGM reading and summary payloads from the same normalized glucose data.

GlucoseIQ can turn normalized readings and Time-in-Range results into plain, serializable health-data objects. The builders are stateless, dependency-free, and do not require a FHIR SDK.

Use this guide when you need to hand glucose data to a clinical system, research pipeline, personal data store, or another standards-aware service.

Builders, not a transport client

GlucoseIQ builds payloads. Your application still owns authentication, patient and device references, consent, server-specific profiles, network requests, retries, and validation against the receiving system.

Choose an output

GoalBuilderResult
Export one CGM reading as FHIRbuildFHIRSensorReadingA lightweight Observation
Export many CGM readings as FHIRbuildFHIRSensorReadingsAn array of Observation objects
Export aggregate CGM metrics as FHIRbuildFHIRCGMSummaryA CGM summary Observation
Export an Open mHealth bodybuildOMHBloodGlucoseA blood-glucose body
Export several Open mHealth bodiesbuildOMHBloodGlucoseListAn array of bodies
Export a wrapped Open mHealth datapointbuildOMHDataPointA header plus blood-glucose body

All builders are available from @glucoseiq/core and the @glucoseiq/core/interop subpath.

1. Start with normalized readings

import type { GlucoseReading } from '@glucoseiq/core'

const readings: GlucoseReading[] = [
  { value: 104, unit: 'mg/dL', timestamp: '2026-07-12T12:00:00Z' },
  { value: 5.9, unit: 'mmol/L', timestamp: '2026-07-12T12:05:00Z' },
]

FHIR and Open mHealth builders preserve the value and unit supplied for each reading. Normalize vendor payloads and validate timestamps before export; these builders intentionally do not discard or repair input.

Export FHIR sensor readings

import { buildFHIRSensorReadings } from '@glucoseiq/core/interop'

const observations = buildFHIRSensorReadings(readings)

observations[0]
// {
//   resourceType: 'Observation',
//   status: 'final',
//   code: { coding: [{ system: 'http://loinc.org', code: '99504-3', ... }] },
//   effectiveDateTime: '2026-07-12T12:00:00Z',
//   valueQuantity: {
//     value: 104,
//     unit: 'mg/dL',
//     system: 'http://unitsofmeasure.org',
//     code: 'mg/dL'
//   }
// }

The builder selects the interstitial-fluid LOINC code for the reading's unit: mass-per-volume for mg/dL, or molar concentration for mmol/L. It emits one Observation per reading and does not wrap the array in a FHIR Bundle.

Export a FHIR CGM summary

Build the analytics once, then pass its five-range Time-in-Range result into the summary builder:

import {
  analyzeGlucose,
  buildFHIRCGMSummary,
  type GlucoseReading,
} from '@glucoseiq/core'

export function buildSummaryObservation(
  readings: GlucoseReading[],
  period: { start: string; end: string },
) {
  const report = analyzeGlucose(readings, { includeProfile: false })

  if (!report.valid || !report.timeInRange) {
    throw new Error('A CGM summary requires valid glucose readings')
  }

  return buildFHIRCGMSummary(report.timeInRange, period, {
    meanGlucose: report.meanGlucose,
    gmi: report.gmi,
    cv: report.cv,
  })
}

const summary = buildSummaryObservation(readings, {
  start: '2026-06-29T00:00:00Z',
  end: '2026-07-12T23:59:59Z',
})

The result is a CGM summary Observation with LOINC-coded components for very low, low, target, high, and very high percentages. The optional mean glucose, GMI, and coefficient of variation become additional components.

Complete the resource for your server

The lightweight output is aligned with the HL7 CGM Implementation Guide, but a receiving server may require fields such as subject, device, resource identifiers, provenance, or a particular profile URL. Add those in your integration layer and validate the final resource against that server.

Send an Observation

After adding the references and metadata required by your FHIR server, transmit the resource using that server's authentication and error-handling policy:

async function createObservation(
  baseUrl: string,
  accessToken: string,
  observation: object,
) {
  const response = await fetch(`${baseUrl}/Observation`, {
    method: 'POST',
    headers: {
      authorization: `Bearer ${accessToken}`,
      'content-type': 'application/fhir+json',
      accept: 'application/fhir+json',
    },
    body: JSON.stringify(observation),
  })

  if (!response.ok) {
    throw new Error(`FHIR server rejected the resource: ${response.status}`)
  }

  return response.json()
}

For bulk ingestion, construct the Bundle type required by your server around the output of buildFHIRSensorReadings.

Export Open mHealth bodies

Use body-only builders when another system supplies its own headers or storage envelope:

import { buildOMHBloodGlucoseList } from '@glucoseiq/core/interop'

const bodies = buildOMHBloodGlucoseList(readings)

bodies[0]
// {
//   blood_glucose: { value: 104, unit: 'mg/dL' },
//   effective_time_frame: { date_time: '2026-07-12T12:00:00Z' },
//   specimen_source: 'interstitial fluid'
// }

Use buildOMHDataPoint when you want the Open mHealth header and schema identifier as well:

import { buildOMHDataPoint } from '@glucoseiq/core/interop'

const dataPoints = readings.map((reading) =>
  buildOMHDataPoint(reading, crypto.randomUUID()),
)

dataPoints[0].header.schema_id
// { namespace: 'omh', name: 'blood-glucose', version: '3.0' }

The builder generates creation_date_time when called. Supply and persist your own stable IDs when repeated exports must refer to the same datapoints.

Test exports as contracts

The payloads are plain objects, so contract tests do not need a FHIR runtime:

import { describe, expect, it } from 'vitest'
import { buildFHIRSensorReading } from '@glucoseiq/core/interop'

describe('CGM export', () => {
  it('emits a UCUM-coded FHIR quantity', () => {
    const observation = buildFHIRSensorReading({
      value: 104,
      unit: 'mg/dL',
      timestamp: '2026-07-12T12:00:00Z',
    })

    expect(observation.valueQuantity).toEqual({
      value: 104,
      unit: 'mg/dL',
      system: 'http://unitsofmeasure.org',
      code: 'mg/dL',
    })
  })
})

Also run representative payloads through the validator or sandbox supplied by the receiving system. A stable local shape and acceptance by a specific server are two different contracts; production integrations need both.

On this page