Menu

How to setup Strava to Azure API for FHIR integration?

Do you need to automatically import data from Strava to Azure API for FHIR? This blog post shows how to create a simple integration from Strava to Azure API for FHIR. Integration enables to convert Strava exercise data to FHIR observation resource and send it to Azure API for FHIR. This is a quick sample which can be extended to support for many FHIR observation profiles.  

Solution overview

Solution utilizes Strava Webhook to subscribe events (Activity) that occur within Strava. Events are converted to FHIR observation before sending to Azure API for FHIR.

undefined

Solution configuration

1. Setup Strava API application

Login to your Strava account and configure your API application

undefined

2. Create subscription validation endpoint to Azure Function

Next we create an Azure Function which handles Strava Webhook subscription validation request. Subscription validation endpoint receives challenge string and verification token from the Strava when webhook is configured. Verification token is a random string which you can determine. Azure Function endpoint verifies that verification token and echoes challenge value back to Strava. You can find more details about Strava Webhook from here.

Example Azure Function identifies GET request as a subscription validation request:

[FunctionName("StravaActivityToFhir")]
public async Task<IActionResult> Run([HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequest req, ILogger log)
{
    if (req.Method == "GET")
    {
        return Verify(req, log);
    }
    else if (req.Method == "POST")
    {
        return await ReceiveAsync(req, log);
    }

    log.LogInformation("Unknown operation");

    return new BadRequestResult();
}

Verify method checks that verification token delivered in the request is the same which founds from the configuration and returns challenge value back.

private IActionResult Verify(HttpRequest req, ILogger log)
{
    string hubChallenge = req.Query["hub.challenge"];
    string verifyToken = req.Query["hub.verify_token"];

    log.LogInformation($"Incoming Strava subcription validation request. Challenge was ${hubChallenge} and verify token was ${verifyToken}.");

    if (string.IsNullOrEmpty(hubChallenge) || string.IsNullOrEmpty(verifyToken))
    {
        return new BadRequestResult();
    }
    var stravaWebhookVerificationToken = Environment.GetEnvironmentVariable("StravaWebhookVerificationToken");

    if (verifyToken != stravaWebhookVerificationToken)
    {
        return new BadRequestResult();
    }

    var response = new SubscriptionValidationResponse()
    {
        HubChallenge = hubChallenge
    };

    return new OkObjectResult(response);
}

Verification functionality is now ready and next we can configure the Webhook.

3. Strava Webhook configuration

Open Postman and create a POST request like below:

undefined

If everything worked you'll get a subscription Id back. Now Webhook is configured and endpoint retrieves event every time when new exercise is added to Strava.

{
    "id": 188306
}

4. Create authorize request using Postman to acquire access token

Webhook event contains only Id of the event so the actual data should be retrieved with another request which requires user's access token. 

Open Postman and configure OAUTH2 authentication request with the following details:

undefined

Authenticate with your Strava credentials:

undefined

After authentication you'll get a token:

undefined

5. Create exercise event receiver endpoint to Azure Function

Endpoint receives Webhook event from the Strava. Like said earlier event contains only the Id of the exercise. ReceiveAsync method orchestrates the following operations: 

- Gets exercise data from Strava

- Converts Strava exercise data (Activity) to observation (FHIR) using Hl7.Fhir.STU3 library

- Gets Azure AD access token to enable access to Azure API for FHIR

- Sends observation (FHIR) to Azure API for FHIR using FhirClient

/// <summary>
/// Method handles incoming Strava event
/// </summary>
/// <param name="req"></param>
/// <param name="log"></param>
/// <returns></returns>
private async Task<IActionResult> ReceiveAsync(HttpRequest req, ILogger log)
{
    var requestBody = await new StreamReader(req.Body).ReadToEndAsync();

    log.LogInformation("Incoming Strava event" + requestBody);

    var data = JsonConvert.DeserializeObject<Event>(requestBody);

    if (data != null)
    {
        if (data.aspect_type == "create")
        {
            var activityData = await GetActivity(data.object_id);
            if (activityData != null)
            {
                var response = await SendDataToFhirRepository(activityData);

                if (response != null)
                {
                    var serializer = new FhirJsonSerializer(new SerializerSettings()
                    {
                        Pretty = true
                    });

                    var resource = serializer.SerializeToString(response);

                    log.LogInformation("Data succesfully sent to FHIR repository: " + resource);
                }
            }
        }
        else if (data.aspect_type == "delete")
        {
            throw new NotImplementedException();
        }

    }
    return new OkObjectResult("OK");
}

5.1 Get full exercise data from Strava

GetActivity method retrieves Strava exercise data by Id. This implementation uses hard coded Strava access token. If you want to use this in production implement proper handling for storing and refreshing tokens.

/// <summary>
/// Retrieves Strava exercise activity by Id
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
private async Task<Activity> GetActivity(long id)
{      
    var stravaBaseUrl = Environment.GetEnvironmentVariable("StravaBaseUrl");
    var stravaActivityEndpoint = Environment.GetEnvironmentVariable("StravaActivityEndpoint");

    //This version uses hard coded access token. TODO: Automate access token persistance and implement refresh token usage
    var accessToken = Environment.GetEnvironmentVariable("StravaToken:AccessToken");

    if (string.IsNullOrEmpty(stravaBaseUrl) || string.IsNullOrEmpty(stravaActivityEndpoint) || string.IsNullOrEmpty(accessToken))
    {
        throw new ArgumentException("Strava configuration values are missing from the configuration");
    }

    var client = new RestClient(stravaBaseUrl)
    {
        Authenticator = new JwtAuthenticator(accessToken)
    };

    var request = new RestRequest(string.Format(stravaActivityEndpoint, id), DataFormat.Json);

    return await client.GetAsync<Activity>(request);
}

Configuration values:

"StravaBaseUrl": "https://www.strava.com",
"StravaActivityEndpoint": "api/v3/activities/{0}",
"StravaToken:AccessToken": "[Your Access Token]"

5.2 Data conversion from Strava Activity to Observation (FHIR)

This example uses IConverter interface to map and convert Strava Activity to Observation resource provided by FHIR. Hl7.Fhir.STU3 library provides full set of FHIR models so mapping is easy to handle.

Converter interface:

public interface IConverter<TSource, TDestination>
{
    TDestination Convert(TSource source_object);
}

Activity to Observation converter:

public class ActivityToObservationConverter : IConverter<Activity, Observation>
{
    public Observation Convert(Activity activity)
    {
        return new Observation()
        {
            Meta = new Meta()
            {
                Profile = new string[] { "http://phr.kanta.fi/StructureDefinition/fiphr-sd-exercisetracking-stu3" }
            },
            Language = "en",
            Text = new Narrative()
            {
                Status = Narrative.NarrativeStatus.Generated,
                Div = $"<div>Time: {activity?.start_date_local} Result: {activity.elapsed_time / 60} min</div>"
            },
            Identifier = new List<Identifier>()
            {
                new Identifier()
                {
                    Use = Identifier.IdentifierUse.Usual,
                    System = "urn:ietf:rfc:3986",
                    Value = "urn:uuid:6288f477-90ef-424a-b6e3-da4ff18a058e"
                },
                new Identifier()
                {
                    Use = Identifier.IdentifierUse.Usual,
                    System = "urn:ietf:rfc:3986",
                    Value = "urn:uuid:00000000-5cb1-fef8-16b6-835409677fb6"
                }
            },
            Status = ObservationStatus.Final,
            Category = new List<CodeableConcept>()
            {
                new CodeableConcept()
                {
                        Coding = new List<Coding>()
                        {
                            new Coding()
                            {
                                System = "http://phr.kanta.fi/fiphr-cs-fitnesscategory",
                                Code = "fitness",
                                Display = "Fitness"
                            }
                        }
                }
            },
            Code = new CodeableConcept()
            {
                Coding = new List<Coding>()
                {
                    new Coding()
                    {
                        System = "http://loinc.org",
                        Code = "55411-3",
                        Display = "Exercise duration"
                    }
                }
            },
            Subject = new ResourceReference()
            {
                Reference = "Patient/" + activity?.athlete?.id,
            },
            Issued = activity?.start_date_local,
            Performer = new List<ResourceReference>()
            {
                new ResourceReference()
                {
                    Reference = "Patient/"+ activity?.athlete?.id
                }
            },
            Value = new Quantity()
            {
                Value = activity.elapsed_time / 60,
                Unit = "min",
                System = "http://unitsofmeasure.org",
                Code = "min"
            }
        };
    }
}

5.2 Get Azure Ad access token

Azure AD access token is required to send data to Azure API for FHIR

/// <summary>
/// Retrieves access token from Azure Ad using app credentials
/// </summary>
/// <returns></returns>
private async Task<string> GetAadAccessToken()
{
    var fhirResource = Environment.GetEnvironmentVariable("FhirResource");
    var azureAdTenant = Environment.GetEnvironmentVariable("AzureAdTenant");
    var azureAdClientId = Environment.GetEnvironmentVariable("AzureAdClientId");
    var azureAdClientSecret = Environment.GetEnvironmentVariable("AzureAdClientSecret");

    if(string.IsNullOrEmpty(fhirResource) || string.IsNullOrEmpty(azureAdTenant) ||
        string.IsNullOrEmpty(azureAdClientId) || string.IsNullOrEmpty(azureAdClientSecret))
    {
        throw new ArgumentException("Azure Ad configuration values are invalid");
    }
    // Authentication using app credentials
    var authenticationContext = new AuthenticationContext(azureAdTenant);
    var credential = new ClientCredential(azureAdClientId, azureAdClientSecret);
    AuthenticationResult authenticationResult = await authenticationContext.AcquireTokenAsync(fhirResource, credential);
    return authenticationResult.AccessToken;
}

Configuration values:

"FhirResource": "https://[Your FHIR Repository].azurehealthcareapis.com",
"AzureAdTenant": "https://login.microsoftonline.com/[Tenant Id]",
"AzureAdClientId": "[Your application Id]",
"AzureAdClientSecret": "[Your application secret]"

5.3 Observation data sending to Azure API for FHIR

SendDataToFhirRepository method wraps data conversion and sending:

/// <summary>
/// Converts Strava activity to FHIR observation and sends it to FHIR repository
/// </summary>
/// <param name="activity"></param>
/// <returns></returns>
private async Task<Observation> SendDataToFhirRepository(Activity activity)
{
    var fhirRepositoryUrl = Environment.GetEnvironmentVariable("FhirRepositoryUrl");

    if (string.IsNullOrEmpty(fhirRepositoryUrl))
    {
        throw new ArgumentException("Fhir repository Url is missing from the configuration");
    }

    var converter = new ActivityToObservationConverter();
    var observation = converter.Convert(activity);
    var accessToken = await GetAadAccessToken();

    var settings = new FhirClientSettings
    {
        PreferredFormat = ResourceFormat.Json,
        PreferredReturn = Prefer.ReturnRepresentation,
    };

    var client = new FhirClient(fhirRepositoryUrl, settings);
    client.RequestHeaders.Add("Authorization", "Bearer " + accessToken);
    return await client.CreateAsync(observation);
}

Configuration values:

"FhirRepositoryUrl": "https://[Your FHIR Repository].azurehealthcareapis.com",

Testing

Login to Strava and add one exercise manually.

undefined

Wait a moment and create a GET request to the Azure API for FHIR to retrieve all data. If everything worked you'll see response like this:

{
    "resourceType": "Bundle",
    "id": "de02a65ebfed8d4193ab2e19a3b2d501",
    "meta": {
        "lastUpdated": "2021-04-04T07:36:53.5823744+00:00"
    },
    "type": "searchset",
    "link": [
        {
            "relation": "self",
            "url": "https://[Your Fhir repository].azurehealthcareapis.com/"
        }
    ],
    "entry": [
        {
            "fullUrl": "https://[Your Fhir repository].azurehealthcareapis.com/Observation/47b4f469-519b-44b8-8d3e-79c29a50ede2",
            "resource": {
                "resourceType": "Observation",
                "id": "47b4f469-519b-44b8-8d3e-79c29a50ede2",
                "meta": {
                    "versionId": "1",
                    "lastUpdated": "2021-04-04T07:36:44.336+00:00",
                    "profile": [
                        "http://phr.kanta.fi/StructureDefinition/fiphr-sd-exercisetracking-stu3"
                    ]
                },
                "language": "en",
                "text": {
                    "status": "generated",
                    "div": "<div>Time: 4/4/2021 9:30:00 AM Result: 80 min</div>"
                },
                "identifier": [
                    {
                        "use": "usual",
                        "system": "urn:ietf:rfc:3986",
                        "value": "urn:uuid:6288f477-90ef-424a-b6e3-da4ff18a058e"
                    },
                    {
                        "use": "usual",
                        "system": "urn:ietf:rfc:3986",
                        "value": "urn:uuid:00000000-5cb1-fef8-16b6-835409677fb6"
                    }
                ],
                "status": "final",
                "category": [
                    {
                        "coding": [
                            {
                                "system": "http://phr.kanta.fi/fiphr-cs-fitnesscategory",
                                "code": "fitness",
                                "display": "Fitness"
                            }
                        ]
                    }
                ],
                "code": {
                    "coding": [
                        {
                            "system": "http://loinc.org",
                            "code": "55411-3",
                            "display": "Exercise duration"
                        }
                    ]
                },
                "subject": {
                    "reference": "Patient/16623445"
                },
                "issued": "2021-04-04T09:30:00+00:00",
                "performer": [
                    {
                        "reference": "Patient/16623445"
                    }
                ],
                "valueQuantity": {
                    "value": 80,
                    "unit": "min",
                    "system": "http://unitsofmeasure.org",
                    "code": "min"
                }
            },
            "search": {
                "mode": "match"
            }
        }
    ]
}

Source code

Source code is available in GitHub.

Comments