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. The integration enables the conversion of Strava exercise data to the FHIR observation resource and sends it to Azure API for FHIR. This is a quick sample that can be extended to support many FHIR observation profiles.  

Solution overview

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

Solution configuration

1. Setup Strava API application

Login to your Strava account and configure your API application

2. Create a subscription validation endpoint to Azure Function

Next, we create an Azure Function that handles Strava Webhook subscription validation requests. The subscription validation endpoint receives a challenge string and verification token from the Strava when the webhook is configured. The verification token is a random string that you can determine. Azure Function endpoint verifies that verification token and echoes the 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();
}

The verify method checks that the verification token delivered in the request is the same as founds from the configuration and returns the challenge value.

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 the below:

If everything works you'll get a subscription ID back. Now Webhook is configured and the endpoint retrieves an event whenever a new exercise is added to Strava.

{
    "id": 188306
}

4. Create authorized request using Postman to acquire an access token

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

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

Authenticate with your Strava credentials:

After authentication you'll get a token:

5. Create exercise event receiver endpoint to Azure Function

The endpoint receives Webhook events from the Strava. As said earlier event contains only the ID of the exercise. The 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 the IConverter interface to map and convert Strava Activity to an Observation resource provided by FHIR. Hl7.Fhir.STU3 library provides a 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 sentLog in 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

Log in to Strava and add one exercise manually.

Wait a moment and create a GET request to the Azure API for FHIR to retrieve all data. If everything works you'll see a 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

The source code is available on GitHub.

Comments