How to export Apple Health data to Azure API for FHIR?

Nowadays Azure has many healthcare-specific services. This blog post introduces you to the FHIR converter and Azure API for FHIR service. I'll show you a simple example of how to export Apple Health data to Azure API for FHIR. 

What is Azure API for FHIR?

The Azure API for FHIR is a managed (PaaS) data platform solution for healthcare organizations that supports the FHIR – Fast Healthcare Interoperability Resources standard. FHIR is a next-generation standards framework created by HL7. Managed Azure API for FHIR solution is built on Microsoft technologies like Azure AD and CosmosDB.  

What is an FHIR converter?

FHIR converter is an open-source project that enables to convert of HL7 and CDA messages to FHIR via an HTTP API endpoint. FHIR converter (handlebars engine) provides a Web UI that enables to creation of mapping templates (HL7 to FHIR, CDA to FHIR) and HTTP API endpoint. There is also Liquid Engine available which can be used via VS Code and from the command line.

Apple Health data is in CDA format so data must be converted to FHIR before sending to Azure API for FHIR.

Architecture of the solution

This simple integration reads a CDA file from blob storage, converts it to FHIR, and uploads it to Azure API for FHIR.

How to export Apple Health data?

Apple Health mobile application provides an export functionality that exports data in CDA format (Clinical Document Architecture document). If you want to export health data, open the Apple Health application and select your profile. In the profile view, there is a button "Export All Health Data". This functionality creates an export.zip file which contains route information in GPX,  export.xml, and export_cda.xml files. Export_cda.xml file is interesting to us because data is already in healthcare standard format (CDA). 

CDA to FHIR conversion

As said earlier CDA data must be converted to FHIR before sending to Azure API for FHIR. This blog post shows the usage of the Handlebars engine because currently, Liquid Engine does not support C-CDA to FHIR conversions. This post is not a deep walkthrough of Handlebar engine details. More details about the FHIR Converter (handlebars engine) in general can be found here. The previously mentioned link contains a lot of information about how to use the handlebar engine and a description of the API endpoints.

How to test CDA to FHIR conversion with FHIR converter (handlebar engine)?

FHIR converter (handlebars) allows you to input example HL7 or CDA data and create the FHIR mapping using handlebars. You're also free to create your own handlebar templates. UI shows the FHIR conversion result in the right section.

Raw CDA data exported from Apple Health looks like this:

<?xml version="1.0"?>
<?xml-stylesheet type="text/xsl" href="CDA.xsl"?>
<ClinicalDocument xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:hl7-org:v3 ../../../CDA%20R2/cda-schemas-and-samples/infrastructure/cda/CDA.xsd" xmlns="urn:hl7-org:v3" xmlns:cda="urn:hl7-org:v3" xmlns:sdtc="urn:l7-org:sdtc" xmlns:fhir="http://hl7.org/fhir/v3">
 <realmCode code="US"/>
 <typeId root="2.16.840.1.113883.1.3" extension="POCD_HD000040"/>
 <templateId root="2.16.840.1.113883.10.20.22.1.2"/>
 <id extension="Health Export CDA" root="1.1.1.1.1.1.1.1.1"/>
 <code codeSystem="2.16.840.1.113883.6.1" codeSystemName="LOINC" code="34109-9" displayName="Note"/>
 <title>Health Data Export</title>
 <effectiveTime value="20210313154208+0200"/>
 <confidentialityCode code="N" codeSystem="2.16.840.1.113883.5.25"/>
 <recordTarget>
  <patientRole>
   <id root="2.16.840.1.113883.4.6" nullFlavor="NA"/>
   <patient>
    <name use="CL">Patient Name</name>
    <administrativeGenderCode code="M" codeSystem="2.16.840.1.113883.5.1" displayName="Male"/>
    <birthTime value="19000101"/>
   </patient>
  </patientRole>
 </recordTarget>
 <entry typeCode="DRIV">
  <organizer classCode="CLUSTER" moodCode="EVN">
   <templateId root="2.16.840.1.113883.10.20.22.4.26"/>
   <id root="e39a8abd-09ee-414a-bac3-03309f6268f6"/>
   <code code="46680005" codeSystem="2.16.840.1.113883.6.96" codeSystemName="SNOMED CT" displayName="Vital signs"/>
   <statusCode code="completed"/>
   <effectiveTime>
    <low value="20201104162503+0200"/>
    <high value="20201104162503+0200"/>
   </effectiveTime>
   <component>
    <observation classCode="OBS" moodCode="EVN">
     <templateId root="2.16.840.1.113883.10.20.22.4.27"/>
     <id root="e39a8abd-09ee-414a-bac3-03309f6268f6"/>
     <code code="8302-2" codeSystem="2.16.840.1.113883.6.1" codeSystemName="LOINC" displayName="Height"/>
     <text>
      <sourceName>Health</sourceName>
      <sourceVersion>14.1</sourceVersion>
      <value>183</value>
      <type>HKQuantityTypeIdentifierHeight</type>
      <unit>cm</unit>
     </text>
     <statusCode code="completed"/>
     <effectiveTime>
      <low value="20201104162503+0200"/>
      <high value="20201104162503+0200"/>
     </effectiveTime>
     <value value="183" unit="cm"/>
     <interpretationCode code="N" codeSystem="2.16.840.1.113883.5.83"/>
    </observation>
   </component>
  </organizer>
 </entry>
 </ClinicalDocument>

Note! FHIR converter with handlebars does not offer a ready-made template which extracts observations from CDA data provided by Apple Health export. In this sample, I'll use the default CDA template (CCD.HBS) which converts data to the bundle resource. This basically means that observation data is added to the bundle resource as an attachment. If we open CCD.HBS template with FHIR converter Web UI we can see that the DocumentRefence template uses gzip and base64 encoding for data handling. This is not an optimal approach. I'll investigate later how to modify templates to create multiple observation resource elements from the original data.

After conversion to FHIR data looks like this:

{
  "resourceType": "Bundle",
  "type": "batch",
  "entry": [
    {
      "fullUrl": "urn:uuid:9c83837d-5acd-3831-b76f-46b6908e7cbd",
      "resource": {
        "resourceType": "Composition",
        "id": "9c83837d-5acd-3831-b76f-46b6908e7cbd",
        "identifier": {
          "use": "official",
          "value": "1.1.1.1.1.1.1.1.1"
        },
        "status": "final",
        "type": {
          "coding": [
            {
              "code": "34109-9",
              "display": "Note",
              "system": "http://loinc.org"
            }
          ]
        },
        "date": "2021-03-13T13:42:08.000Z",
        "title": "Health Data Export",
        "confidentiality": "N",
        "subject": {
          "reference": "Patient/ee80fe9e-15f2-3c90-bea4-9188eea3e031"
        }
      },
      "request": {
        "method": "PUT",
        "url": "Composition/9c83837d-5acd-3831-b76f-46b6908e7cbd"
      }
    },
    {
      "fullUrl": "urn:uuid:ee80fe9e-15f2-3c90-bea4-9188eea3e031",
      "resource": {
        "resourceType": "Patient",
        "meta": {
          "profile": [
            "http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient"
          ]
        },
        "id": "ee80fe9e-15f2-3c90-bea4-9188eea3e031",
        "birthDate": "1900-01-01",
        "gender": "male"
      },
      "request": {
        "method": "PUT",
        "url": "Patient/ee80fe9e-15f2-3c90-bea4-9188eea3e031"
      }
    },
    {
      "fullUrl": "urn:uuid:5647bcb4-c3fa-31e4-8238-46d7b503f5a3",
      "resource": {
        "resourceType": "DocumentReference",
        "id": "5647bcb4-c3fa-31e4-8238-46d7b503f5a3",
        "type": {
          "coding": [
            {
              "code": "34109-9",
              "display": "Note",
              "system": "http://loinc.org"
            }
          ]
        },
        "date": "2021-03-21T11:00:24.138Z",
        "status": "current",
        "content": [
          {
            "attachment": {
              "contentType": "text/plain",
              "data": "H4sIAAAAAAAACp1WW2/iOBR+n19hRZqnVWI7CZmAAqNZ6G6roXSmMNW+rdzENJZyQbbDZX/9HucCgWmX6QKqHJ+Lv/OdzyeNPu/zDG25VKIsxhZ1iPV58iEyu7bSh4yrlHON9GHDx5bme433KrNQKvl6bE1nXxzzaCKmmShEzLJZGVc5LzSCDIUa7ZUYW6nWmxHGu93O2XlOKV+wSwjFf93Pl3HKc2aLQmlWxNxC4D9S9ea8jJmuQVWyGKXZJxsCR1sPOQ5ufnD8R5c8ujhOmN0EKZsVia1YvgHkWBRryZSWVawryY0bbiAnVgPvMne7PQLPt0wq0XFja01mozOuUyGP5UJsXavZxJBg8gFFkrMsn5YJRzH8GVs/lhY2+4bfuwTJstRjy3Vo4IQ+cahDqReGHizgfCCfF02bvj1MZ3/fzgh8fNJm4FAz0/+ZhTgu/MDguE2QSPpZbwGcTtHNflNKjYApq01FnYtvEx13dSwPCo5/7cgAfHsuC5ZD0fOHu8XUahnwfEqG9tBCiVBQwKFxWZSat3UJnfFJC23GNGvxRbixgAtfr3msxZavRM7RlmUVZHCJS4lHPTrwXRL+RkBxHepiLRJQqGCZ0IdeMxbWlWoGjjtokkgelzJZMfnCNTyjaANahZyPZQ0J1dS+1QffCSxUVFn2R8a2JQhm8aXOekpTP6CoAC5QpQDadG5NvjU2ZBiKsLG1bizJ4e6Bzpkh4U9eJFz2yrq/Xha9oP+eZbyFhKJnIXXap5YOQXeU0A407qE+PrRMRPiCqghs8lAPlGkNb/Z492TVoXBZWCH+4RLFGVOqMU/nP5arm0cL5WWZNFs3TwurOfk9ovcdN+gQH5vDvSEL2XNikyHntk99Zj+z2LOJ55HhOnCDcH0Mik+U+kEQAgeDa8wGzjD4Wf/LxcP9zQxNVxesPwnNMqTES6G6M2Es6kr1mhmXZrRpnnQeZ+JvO5aVu941IJQSnwbugHina2DcUvGSXveL8M9HRAZGWZyUWj4rLrf1vO437+H35auNe3/rPnWo/1/zztsXesS13evNe3t2nfXtlgOR+nSSeU22a+hgWcmYG8d2iEW4t3Xu9dS8hifUd2jn1u11nnW/JjT0ItwsO4O5UZPbr98rBqNNH1b1G8WMubXgsoEIM9M4dRFVIfQkziNcL1rwuIf+uvxeF+AvS/CXRfi6DDs6joMphNekKQaA5j3BFJrLjeS61uf7Bj5k7K4L7om8vRln1wAcugFWD7560NWry/+NJv8Cd2qLzXYJAAA=",
              "hash": "4I5hBk72qplf4duGUO1HoWYE2lM="
            }
          }
        ]
      },
      "request": {
        "method": "PUT",
        "url": "DocumentReference/5647bcb4-c3fa-31e4-8238-46d7b503f5a3"
      }
    }
  ]
}

Why I'm using an FHIR converter from a local machine?

I'm using an FHIR converter from my local machine via NGROK because I didn't manage to get a node.js-based application working in Azure App Service. NGROK basically exposes my local FHIR conversation API endpoint behind NATs and firewalls to the public internet over secure tunnels.

Azure service configurations

Azure API for FHIR and Azure AD

First, we need Azure API for the FHIR service. You can configure this service via Azure Portal or CLI. 

Next, we need to register an application to Azure AD which is used to get Azure AD tokens from Logic Apps. Azure AD token is required to have access to Azure API for the FHIR endpoint. You can follow this guide to register your application. After registration open Azure API for FHIR and add the ex. the following role assignment to your application.

Test access token fetch with Postman

Before continuing create a token endpoint request with your application credentials.

Orchestration of the integration

I'll use Logic App to orchestrate reading the CDA data, calling FHIR conversion API, fetching Azure AD token, and sending data to API for FHIR.

1. STEP: Recurrence

The logic app is triggered once a month.

2. STEP: Get blob content

CDA export file content is fetched from the Blob Storage

3. STEP: Convert CDA to FHIR

This action sends blob file content to FHIR converter API which is in my local machine. In the URI is determined that the datatype is "CDA" and the template is "CCD.HBS". The body contains CDA data in XML format.

4. STEP: Skip root element

FHIR converter API endpoint returns converted FHIR data inside the "fhirResource" element. This step skips the root element and returns only child elements because otherwise, Azure API for the FHIR endpoint does not work.

{
  "fhirResource": {
    "resourceType": "Bundle",
    "type": "batch",
    "entry": []
  }
}

5. STEP: Get Access Token

Integration needs an Azure AD access token to communicate with Azure API for FHIR. This request gets an access token from the Azure AD token endpoint.

6. STEP: Parse Access Token response

Parse JSON action parses access token response. After that data is more easily accessible in the next actions of the Logic App.

7. STEP: Send data to API for FHIR

The last step sends converted CDA data in the FHIR format to the Azure API for the FHIR endpoint. An access token is added as a bearer token to the request.

The logic app template is available from my Github repository.

Create a query to API for the FHIR endpoint

Create the following GET request to the API for FHIR to retrieve data that was sent earlier. Remember to add a bearer token to the request.

Comments