TL;DR

Fitbits are a commonly used brand of activity tracker that collect a large volume of data regarding individuals physical activity. Fitbit provide a web API that allows users to download their data allowing much more complex analysis to be performed. This post will walk you through how to access summary data from the web API as well as how to access finer grain, intra-day data.

Introduction

A key focus of my PhD is the use of digital health technologies and how these can be used to both quantify physical behaviours as well as promote behaviour change. Fitbit are a leading brand in digital health technologies space and their range of physical activity trackers have a wide user base. Due to their prominence, and extensive list of features, I will be using as part of my PhD to collect data on participants physical activity levels. To do this, I will need to extract data from the devices. Some data can be downloaded as a .csv file by following the instructions outlined on the Fitbit website. However, we may want a greater range, or higher resolution, of data than that available from the prior link. To do this, we can use the Fitbit Web API. This process is a little more complicated but this psot will hopefully simplify the steps involved.

What is the Fitbit API?

The following paragraph gives a relatively complex summary of what an API is. You DO NOT need to know any of this to retrieve your data, but if you are interested please feel free to give it a read.

An API (Application Programming Interface) is a software intermediary that allows two applications to communicate with each other. The Fitbit API is an HTTP API that allows individuals to send requests, and data is returned. The API adds a layer of security when retrieving data. To access users Fitbit data securely, an OAUth 2.0 Authorisation Framework is utilised. There are two methods to obtain authorisation to access data within the Fitbit API: Authorisation Code Grant Flow (more secure, but more but complex to set up) or the Implicit Grant Flow (lower security but more than sufficient for our purposes). As we are only accessing our own data, we will use the implicit grant flow approach.

Creating an App

To get access to our data, we first need to create an application in our Fitbit account. This can be done by logging into the Fitbit development page. The steps that need to be followed are well outlined here so I will not repeat them. Once the app is created, we need to authorise it to access our Fitbit data.

Authorising Access to the Fitbit API

To authorise the app, we need to click on the “Manage My App” tab and then select the app you want to authorise. Next, scroll down to a link that reads OAuth 2.0 tutorial page. Click this link and then follows the steps within it. Again, a more complete tutorial is provided here so I will not go into excessive detail.

At the end of this process, you should have an OAuth 2.0 access token. This is all we need to access our data.

Getting access to our data

Daily summary data

Once the app is authorised, we are now ready to access our data. To do this, we will use the httr package. To start with, we need to create a header using our access token to authorise us to get our data. We will do that with the code below. You would enter your access code where it says “access_code” however you want to keep this private so I will not post mine on here.

library(httr)
library(tidyverse)
auth_code <- paste("Bearer", "access_code")

No we have the header created, we can use the GET function from the httr package to access the API. Once we have run this line of code, we can pass this to the content function from the httr package to return our data. We set the as argument to parsed to parse the output into an R object where possible. Lets get our sedentary minutes for a given day.

sed_response <- GET(url = "https://api.fitbit.com/1/user/-/activities/minutesSedentary/date/2021-10-07/2021-10-07.json", add_headers(Authorization = auth_code)) %>% 
  content("parsed")

This code produces a list. We can then subset to the element of the list we want using the $ operator followed by the name of the list. Finally, we can then use the bind_rows function from dplyr to bind the list into a dataframe.

sed_response$`activities-minutesSedentary` %>%
  bind_rows()
## # A tibble: 1 × 2
##   dateTime   value
##   <chr>      <chr>
## 1 2021-10-07 1177

Now we have our dataframe that contains the number of sedentary minutes we performed. We can repeat the same steps to get a whole range of other data including lightly active minutes, steps and calories. With this basic boiler plate we can retrieve any data that is summarised daily.

Intra-day data

Some variables, such as heart rate can be retrieved at an intra-day level, meaning we can get data from multiple time points throughout the day. For our purpose, we will look to access our heart rate data every minute. To do this, we can use a very similar series of code as above with a few key changes.

Let’s start by accessing our heart rate data for one day, the 7th October 2021. To do this, we will access the API using the GET command and then parse this data using the content command.

heartrate_response <- GET(url = "https://api.fitbit.com/1/user/-/activities/heart/date/2021-10-07/1d/1min.json", add_headers(Authorization = auth_code)) %>%
              content("parsed")

Now we have retrieved the data, we will use the bind_rows command to create a dataframe containing our heart rate data.

heartrate_response$`activities-heart-intraday`$dataset %>%
  bind_rows() %>%
  head()
## # A tibble: 6 × 2
##   time     value
##   <chr>    <int>
## 1 13:38:00    92
## 2 13:39:00    74
## 3 13:40:00    67
## 4 13:41:00    68
## 5 13:42:00    93
## 6 13:43:00   101

And there we go! Minute by minute heart rate data. We can increase or decrease the resolution of our data by replacing the “1min” with “1sec” or “5min” in the GET URL.

However, how can we get data for more than one day without copying and pasting these lines of code over and over? We can use a for loop to iterate through multiple dates. THe code for this is included below.

hr_list <- list()
for (i in seq(from = as.POSIXct("2021-10-06", format = "%Y-%m-%d"), to = as.POSIXct("2021-10-14", format = "%Y-%m-%d"), by = "day")) {
  multi_heartrate_response <- GET(url = paste0("https://api.fitbit.com/1/user/-/activities/heart/date/", strftime(as.POSIXct(i, origin = "1970-01-01"), format = "%Y-%m-%d"), "/1d/1min.json"), add_headers(Authorization = auth_code)) %>%
    content("parsed")
  if (length(multi_heartrate_response$`activities-heart-intraday`$dataset) == 0) {
    next()
  } else {
    heart_json <- multi_heartrate_response$`activities-heart-intraday`$dataset %>%
      bind_rows() %>%
      mutate(time = paste(strftime(as.POSIXct(i, origin = "1970-01-01"), format = "%Y-%m-%d"), time))
    hr_list <- rbind(hr_list, heart_json)
  }
}

This code get’s slightly more complicated so let’s walk through it line by line. We start off by instantiating an empty list (hr_list) which we will then assign our data collected in the for loop into. Next, we will start writing the for loop. We use the seq() command to create the list of dates. We give the from and to arguments the start and end date respectively (we provide these as POSIXct object and specify their format) and finally provide the by argument with how frequently we would like the dates the be generated (in our case daily). Now we have set up the for loop, we can use the GET and content command as we did before to return the intra-day heart rate data. The only difference is, instead of providing a date as character string, we use the paste0 command to paste the URL together and providing the date by supplying the i variable into the as.POSIXct function. We also wrap this in the strftime function so only the data is pasted and not the time stamp.

We now need to check whether data is actually present for the given day. To do this, we use an if statement to ensure the length of the list of data is greater than 0 (not empty). If the list is empty, we use the next command to tell the for loop to jump to the next date. If the list is not empty, we use the bind_rows() command to create a dataframe with the intra-day heart rate data. Next, we mutate the time column so that it has the date appended at the front. We use the same as.POSIXct command with strftime and paste this along with the time so we know which day, and what time, the data relates to. Finally, using the rbind function we append the new data with the current contents of hr_list. And that’s it! We can now obtain our intra-day heart rate data for any series of dates with a few lines of code!

Whilst the code above is useful, the user must be relatively competent in R to understand and run the code. Luckily, Shiny apps allow users who do not know the R programming language to utilise it’s power in a simple user interface. Therefore, we will now build this code into a basic Shiny app.

Make it Shine

The app is relatively straight forward, we will start by defining the user interface (UI) followed by defining the server logic. As with most of my posts, this is not meant as a tutorial to creating Shiny apps but more as an overview. I will however try to highlight anything that I think may be useful/interesting to others.

The User Interface (UI)

Let’s begin by looking at the code that generates the UI. For this shiny app we will use the fluid page set up with a side bar panel for our inputs and a tabset main panel to display our multiple outputs. The basic code for this layout is below.

library(shiny)
ui <- fluidPage(
  titlePanel("Application title"),
  sidebarLayout(
    sidebarPanel(
      # content to include in side bar
      ),
    mainPanel(
      tabsetPanel(
        id = "data_output", type = "tabs",
        tabPanel(
          # content to include in first tab panel 
          ),
        tabPanel(
          # content to include in second tab panel
          )
        )
      )
    )
  )

In the code above we create the fluid page, then add a title panel to display a title at the top of the page. Next, we use the side bar layout function to tell shiny we want our application to have a side bar and a main panel. We then use the side bar panel function to create the side bar and within this function we can include the content of our side bar. Then, we use the main panel function to create the main panel in our app. In our app, we want to use a tabset panel, this gives us tabs at the top of the page that allows us to switch between pages in the main panel whilst keeping the side bar the same. We then specify the id of the tabset panel and the type of tabs we want to use (this step is optional). Finally, we use the tab panel function to create the tab panels within our main panel. Inside each of these function we will enter the code to create our outputs.

Now we understand the basic structure of lets look at apps UI. This is long block of code so we will break it down into two natural sections. The side bar inputs and then the main panel outputs. Below is the code to generate our inputs within our side bar.

ui <- fluidPage(
  titlePanel("Fitbit data from web API"),
  sidebarLayout(
    sidebarPanel(
      useShinyjs(),
      "To retrieve your client ID, please visit",  HTML("<a href = http://dev.fitbit.com/apps>this link</a>"),
      "sign into your Fitbit account and register an app.",
      br(),
      br(),
      "Then click on \"Manage Apps\" and copy and paste the client ID into the box below.",
      br(),
      br(),
      "More detailed instructions on registering a Fitbit app an be found",
      HTML("<a href = https://towardsdatascience.com/using-the-fitbit-web-api-with-python-f29f119621ea>here</a>"),
      "specifically at the beginning of step 2",
      br(),
      br(),
      textInput("client_id", label = "Please enter the client ID below:", value = ""),
      textInput("callback_url", label = "Please enter the callback URL (which you entered when creating the application on the Fitbit website)"),
      "When you click the button below, you will be redirected to a new webpage. Please provide permission for the app you just created to
      access your data. A link will then appear below the button, click it, and it will take you to a webpage to give the app permission to
      access your data.",
      br(),
      br(),
      actionButton("go_to_url", "Authorise app"),
      br(),
      uiOutput("url"),
      br(),
      "Please enter the URL you were redirected to once you gve permission for the app to access your data below.",
      textInput("returned_url", label = "Please enter the returned URl from pressing the button above:", value = ""),
      actionButton("get_access_token", "Get access token"),
      dateInput("start_date", label = "Please enter the first date you would like to return data from:", value = today(), format = "yyyy-mm-dd"),
      dateInput("end_date", label = "Please enter the last date you would like to return data from:", value = today(), format = "yyyy-mm-dd"),
      actionButton("get_data", "Get data"),
      br(),
      downloadButton("download_data", label = "Download data")
    ) # TRUNCATED! The main panel code would follow on from here

This is still a substantial amount of code but it is relatively straight forward. We start by using the function useShinyjs which allows us to use javascript functions written in shinyjs package (we will come back to this later). We then start by simply writing some text to explain to users how to get their client ID. We use the HTML function from shiny to add a link into our text (this requires a small amount of HTML knowledge but there are plenty of resources online that explain this in good detail). We then use the br function. This is the Shiny equivalent of the br HTML tag which adds a new line. We use this throughout the app to control the spacing of objects in our side bar. The next few lines just add text and HTML links to explain the process of getting our access token.

You may notice in the line that begins “Then click on”Manage Apps” we wrap the entire line in “” to indicate to R it is text. However, when I then want to use “” to indicate a quote from the website I use a backslash before each speech mark. This is necessary otherwise R will think we are trying to signify two series of text and not print the speech mark in the output. The backslash in R is the escape character meaning it treats anything after it as a normal character rather than interpreting it as code.

Further down the code, we start to add inputs to the side bar. In this app, we only use text or date inputs however there is a large range of input types available. The basic arguments taken by an input is the inputId (which allows us to reference the input later in the app), a label (displayed to the user so they understand what should be put into the app) and often a value (what value would you like the input to display by default). Most inputs take a range of additional inputs which is helpful for customisation.

You will also notice we use the actionButton function as well as the downloadButton function. These create buttons that users can click. Again, they take an inputId and label argument. These are self-explanatory, we use them so a user can control when a certain action is performed (we will come back to this later as well!).

The final piece of side bar code I want to cover is the uiOutput function. This function allows us to dynamically generate parts of our Ui based on user inputs within the UI. In our app, we use it to dynamically generate a URL based on user’s input. We will walk through the code to generate this later in the server function.

That is all the side bar code! It’s lengthy but not too complicated once you get the basic idea. Luckily, the main panel code is much shorter!

mainPanel(
      tabsetPanel(
        id = "data_output", type = "tabs",
        tabPanel(
          "Daily summary", withSpinner(dataTableOutput("daily_summary"),
                                       type = 3, color = "#5785AD",
                                       color.background = "white"
          )),
          tabPanel("Heart rate", withSpinner(dataTableOutput("hr"),
                                                    type = 3, color = "#5785AD",
                                                    color.background = "white")),
        tabPanel("Step", withSpinner(dataTableOutput("step"),
                                                  type = 3, color = "#5785AD",
                                                  color.background = "white")),
        tabPanel("Calories", withSpinner(dataTableOutput("calorie"),
                                     type = 3, color = "#5785AD",
                                     color.background = "white")),
        tabPanel("Distance", withSpinner(dataTableOutput("distance"),
                                         type = 3, color = "#5785AD",
                                         color.background = "white")),
        tabPanel("Floors", withSpinner(dataTableOutput("floor"),
                                         type = 3, color = "#5785AD",
                                         color.background = "white")),
        tabPanel("Elevation", withSpinner(dataTableOutput("elevation"),
                                       type = 3, color = "#5785AD",
                                       color.background = "white"))
        )

In the code above, we specify the main panel and then tell it to use a tabset design. We then provide it seven tab panels. This means it will create seven panels with different outputs. In each tab panel we specify the name for the tab panel followed by the output we want to be included in tab panel. In our app we use the dataTableOutput function which will generate a table of the dataframe (we will create this in the server code later).

I also use the withSpinner function to wrap these outputs in. This is a function from the shinycssloaders package. This function produces a spinning wheel whilst the output is generated. I think this adds significantly to the user experience as they can understand the app is doing something and it may take a while, rather than starring at a blank screen wondering what is going on! Within this function, we specify the type of spinner (check the package documentation to understand the differences), the colour of the spinner and the background colour.

That is the end of our UI logic for the app! The full code is included below.

ui <- fluidPage(
  titlePanel("Fitbit data from web API"),
  sidebarLayout(
    sidebarPanel(
      useShinyjs(),
      "To retrieve your client ID, please visit",  HTML("<a href = http://dev.fitbit.com/apps>this link</a>"),
      "sign into your Fitbit account and register an app.",
      br(),
      br(),
      "Then click on \"Manage Apps\" and copy and paste the client ID into the box below.",
      br(),
      br(),
      "More detailed instructions on registering a Fitbit app an be found",
      HTML("<a href = https://towardsdatascience.com/using-the-fitbit-web-api-with-python-f29f119621ea>here</a>"),
      "specifically at the beginning of step 2",
      br(),
      br(),
      textInput("client_id", label = "Please enter the client ID below:", value = ""),
      "When you click the button below, you will be redirected to a new webpage. Please provide permission for the app you just created to
      access your data. A link will then appear below the button, click it, and it will take you to a webpage to give the app permission to
      access your data.",
      br(),
      br(),
      actionButton("go_to_url", "Authorise app"),
      br(),
      uiOutput("url"),
      br(),
      "Please enter the URL you were redirected to once you gve permission for the app to access your data below.",
      textInput("returned_url", label = "Please enter the returned URl from pressing the button above:", value = ""),
      actionButton("get_access_token", "Get access token"),
      dateInput("start_date", label = "Please enter the first date you would like to return data from:", value = today(), format = "yyyy-mm-dd"),
      dateInput("end_date", label = "Please enter the last date you would like to return data from:", value = today(), format = "yyyy-mm-dd"),
      actionButton("get_data", "Get data"),
      br(),
      downloadButton("download_data", label = "Download data")
    ),
    mainPanel(
      tabsetPanel(
        id = "data_output", type = "tabs",
        tabPanel(
          "Daily summary", withSpinner(dataTableOutput("daily_summary"),
                                       type = 3, color = "#5785AD",
                                       color.background = "white"
          )),
          tabPanel("Heart rate", withSpinner(dataTableOutput("hr"),
                                                    type = 3, color = "#5785AD",
                                                    color.background = "white")),
        tabPanel("Step", withSpinner(dataTableOutput("step"),
                                                  type = 3, color = "#5785AD",
                                                  color.background = "white")),
        tabPanel("Calories", withSpinner(dataTableOutput("calorie"),
                                     type = 3, color = "#5785AD",
                                     color.background = "white")),
        tabPanel("Distance", withSpinner(dataTableOutput("distance"),
                                         type = 3, color = "#5785AD",
                                         color.background = "white")),
        tabPanel("Floors", withSpinner(dataTableOutput("floor"),
                                         type = 3, color = "#5785AD",
                                         color.background = "white")),
        tabPanel("Elevation", withSpinner(dataTableOutput("elevation"),
                                       type = 3, color = "#5785AD",
                                       color.background = "white"))
        )
      )
    )
)

The server logic

Now we have explored the UI for the app, let’s examine the server logic. Server logic in a Shiny app is where all the R code is written to perform the necessary analysis. Again, this will not be a full tutorial but I will try and highlight some of the more useful elements of this code.

Let’s look at the first step of our server code.

rv <- reactiveValues(
    url = NULL, 
    auth_code = NULL,
    daily_summary = NULL,
    hr_list = NULL,
    heartrate_response = NULL,
    heart_json = NULL,
    step_list = NULL,
    step_response = NULL,
    step_json = NULL,
    calorie_list = NULL,
    calorie_response = NULL,
    calorie_json = NULL, 
    distance_list = NULL,
    distance_response = NULL,
    distance_json = NULL, 
    floor_list = NULL,
    floor_response = NULL,
    floor_json = NULL, 
    elevation_list = NULL,
    elevation_response = NULL,
    elevation_json = NULL, 
    intraday_summary = NULL
  )

We start our app by defining a whole load of reactive values. These are values that can then have assigned into them later in the code. We name them all here so it is easier to keep track of what happens to each variable as the app progresses.

observeEvent(input$go_to_url, {
      rv$url <- get_access_token_url(client_id = input$client_id)
      output$url <- renderUI({
          url <- a("Link to Fitbit authorisation", href=rv$url, target = "_blank")
          HTML(paste(url))
      })
  })

The code above use two observeEvent statements, these look for a given input, once that input has been received the function executes the code. In the first statement, we look for the go_to_url input (our first action button) to be pressed. When this is pressed, we run the get_access_token_url function (shown below). This function pastes together a URL from the redirect URL specified by the user and their client ID. We then use the renderUI function to render a clickable URL link which is displayed underneath the action button the user just pressed.

observeEvent(input$go_to_url, {
    rv$url <- (get_access_token_url(client_id = input$client_id, callback_url = input$callback_url))
    output$url <- renderUI({
      url <- a("Link to Fitbit authorisation", href = rv$url, target = "_blank")
      HTML(paste(url))
    })
  })

The user then follows the link, provides access to their data and then copy and paste the returned URL into the next text input before pressing the next action button. When the next action button is pressed, the code below is executed which runs the get_access_token function (shown below) which uses regular expressions to extract the authorisation code from the returned URL string. This is then pasted together with the term “Bearer” to created the authorisation code.

get_access_token <- function(returned_url){
  return(str_extract(returned_url, "(?<=#access_token=).*?(?=&user_id)"))
}
 observeEvent(input$get_access_token, {
    rv$auth_code <- paste("Bearer", get_access_token(input$returned_url))
  })

Now we have our access token we can provide this to our code to retrieve our data. We will use the code above to retrieve our daily summary data. Once we have the sedentary, light, moderate and vigorous minutes data we use the full_join function from dplyr to create a single dataframe with all the variables included within it. All this code is wrapped in the function get_active_minutes. This is done so a separate R script can be created which stores this code and then the Shiny app can call the get_active_minutes function to access all this data at once. I highly recommend when creating Shiny apps as soon as blocks of code to accomplish a task grow much longer than a few lines that you pull them out of your main app.r (over server.r) file and store them in a folder; future you will be thankful!

You will notice we use slightly different code to create the dataframe from our retrieved data. Instead of using the bind rows function from dplyr we use a series of base R functions instead. First, we use the data.frame function as we ultimately want a dataframe to be created. Inside this function we use the matrix command to create a matrix. We then supply the matrix function with the data returned from the Fitbit API and we unlist this data so it is entered to the matrix as a succession of numbers. We then set a few parameters for the matrix namely that it should have two columns and the data should be entered by row. We then pass the stringAsFactor parameter to our dataframe and set this to FALSE so that strings are not converted to factors. Finally, we pipe this dataframe into the dplyr rename function and give the columns more useful names. For anyone wondering, there is no reason to use this method over dplyr bind_rows. However, I wanted to show you both so you can see how a problem can be tackled in a variety of ways.

get_active_minutes <- function(auth_code, start_date, end_date){
  
  # sedentary minutes
  
  sed_response <- GET(url = paste0("https://api.fitbit.com/1/user/-/activities/minutesSedentary/date/", start_date, "/", end_date, ".json"), add_headers(Authorization = auth_code))
  
  json_sed <- content(sed_response, "parsed")
  
  sed_dataset <- json_sed$`activities-minutesSedentary`
  sed <- data.frame(
    matrix(
      unlist(sed_dataset),
      ncol=2, byrow=T),
    stringsAsFactors = FALSE) %>% 
    rename(date = X1, sedentary_minutes = X2) 
  
  # lightly active minutes         
  
  light_response <- GET(paste0("https://api.fitbit.com/1/user/-/activities/minutesLightlyActive/date/", start_date, "/", end_date, ".json"), add_headers(Authorization = auth_code))
  
  json_light <- content(light_response, "parsed")
  
  light_dataset <- json_light$`activities-minutesLightlyActive`
  light <- data.frame(
    matrix(
      unlist(light_dataset),
      ncol=2, byrow=T),
    stringsAsFactors = FALSE)%>% 
    rename(date = X1, light_minutes = X2) 
  
  # fairly active minutes         
  
  fair_response <- GET(paste0("https://api.fitbit.com/1/user/-/activities/minutesFairlyActive/date/", start_date, "/", end_date, ".json"), add_headers(Authorization = auth_code))
  
  json_fair <- content(fair_response, "parsed")
  
  fair_dataset <- json_fair$`activities-minutesFairlyActive`
  fair <- data.frame(
    matrix(
      unlist(fair_dataset),
      ncol=2, byrow=T),
    stringsAsFactors = FALSE)%>% 
    rename(date = X1, fair_minutes = X2) 
  
  # very active minutes         
  
  very_response <- GET(paste0("https://api.fitbit.com/1/user/-/activities/minutesVeryActive/date/", start_date, "/", end_date, ".json"), add_headers(Authorization = auth_code))
  
  json_very <- content(very_response, "parsed")
  
  very_dataset <- json_very$`activities-minutesVeryActive`
  very <- data.frame(
    matrix(
      unlist(very_dataset),
      ncol=2, byrow=T),
    stringsAsFactors = FALSE)%>% 
    rename(date = X1, very_minutes = X2) 
  
  # make data frame 
  
  return(full_join(sed, light, by = c("date" = "date")) %>% 
           full_join(fair) %>%
           full_join(very))
  
}

Now we have the longest block of code in our server function.

observeEvent(input$get_data, {
      rv$daily_summary <- get_active_minutes(auth_code = rv$auth_code, start_date = input$start_date, end_date = input$end_date)
      
      for (i in seq(from = as.POSIXct(input$start_date, format = "%Y-%m-%d"), to = as.POSIXct(input$end_date, format = "%Y-%m-%d"), by = "day")) {
          rv$heartrate_response <- GET(url = paste0("https://api.fitbit.com/1/user/-/activities/heart/date/", strftime(as.POSIXct(i, origin = "1970-01-01"), format = "%Y-%m-%d"), "/1d/1min.json"), add_headers(Authorization = rv$auth_code)) %>%
              content("parsed")
          if(length(rv$heartrate_response$`activities-heart-intraday`$dataset) == 0){
              next()
          } else{
          rv$heart_json <- rv$heartrate_response$`activities-heart-intraday`$dataset %>% 
              bind_rows() %>%
              mutate(time = paste(strftime(as.POSIXct(i, origin = "1970-01-01"), format = "%Y-%m-%d"), time))
              rv$hr_list <- rbind(rv$hr_list, rv$heart_json)
          }
      }
      
      for (i in seq(from = as.POSIXct(input$start_date, format = "%Y-%m-%d"), to = as.POSIXct(input$end_date, format = "%Y-%m-%d"), by = "day")) {
        rv$step_response <- GET(url = paste0("https://api.fitbit.com/1/user/-/activities/steps/date/", strftime(as.POSIXct(i, origin = "1970-01-01"), format = "%Y-%m-%d"), "/1d/1min.json"), add_headers(Authorization = rv$auth_code)) %>%
          content("parsed")
        if(length(rv$step_response$`activities-steps-intraday`$dataset) == 0){
          next()
        } else{
          rv$step_json <- rv$step_response$`activities-steps-intraday`$dataset %>% 
            bind_rows() %>%
            mutate(time = paste(strftime(as.POSIXct(i, origin = "1970-01-01"), format = "%Y-%m-%d"), time))
          rv$step_list <- rbind(rv$step_list, rv$step_json)
        }
      }
      
      for (i in seq(from = as.POSIXct(input$start_date, format = "%Y-%m-%d"), to = as.POSIXct(input$end_date, format = "%Y-%m-%d"), by = "day")) {
        rv$calorie_response <- GET(url = paste0("https://api.fitbit.com/1/user/-/activities/calories/date/", strftime(as.POSIXct(i, origin = "1970-01-01"), format = "%Y-%m-%d"), "/1d/1min.json"), add_headers(Authorization = rv$auth_code)) %>%
          content("parsed")
        if(length(rv$calorie_response$`activities-calories-intraday`$dataset) == 0){
          next()
        } else{
          rv$calorie_json <- rv$calorie_response$`activities-calories-intraday`$dataset %>% 
            bind_rows() %>%
            mutate(time = paste(strftime(as.POSIXct(i, origin = "1970-01-01"), format = "%Y-%m-%d"), time))
          rv$calorie_list <- rbind(rv$calorie_list, rv$calorie_json)
        }
      }
      
      for (i in seq(from = as.POSIXct(input$start_date, format = "%Y-%m-%d"), to = as.POSIXct(input$end_date, format = "%Y-%m-%d"), by = "day")) {
        rv$distance_response <- GET(url = paste0("https://api.fitbit.com/1/user/-/activities/distance/date/", strftime(as.POSIXct(i, origin = "1970-01-01"), format = "%Y-%m-%d"), "/1d/1min.json"), add_headers(Authorization = rv$auth_code)) %>%
          content("parsed")
        if(length(rv$distance_response$`activities-distance-intraday`$dataset) == 0){
          next()
        } else{
          rv$distance_json <- rv$distance_response$`activities-distance-intraday`$dataset %>% 
            bind_rows() %>%
            mutate(time = paste(strftime(as.POSIXct(i, origin = "1970-01-01"), format = "%Y-%m-%d"), time))
          rv$distance_list <- rbind(rv$distance_list, rv$distance_json)
        }
      }
      
      for (i in seq(from = as.POSIXct(input$start_date, format = "%Y-%m-%d"), to = as.POSIXct(input$end_date, format = "%Y-%m-%d"), by = "day")) {
        rv$floor_response <- GET(url = paste0("https://api.fitbit.com/1/user/-/activities/floors/date/", strftime(as.POSIXct(i, origin = "1970-01-01"), format = "%Y-%m-%d"), "/1d/1min.json"), add_headers(Authorization = rv$auth_code)) %>%
          content("parsed")
        if(length(rv$floor_response == 1)){
          break()
        } else{
          if(length(rv$floor_response$`activities-floors-intraday`$dataset) == 0){
            next()
          } else{
            rv$floor_json <- rv$floor_response$`activities-floors-intraday`$dataset %>% 
              bind_rows() %>%
              mutate(time = paste(strftime(as.POSIXct(i, origin = "1970-01-01"), format = "%Y-%m-%d"), time))
            rv$floor_list <- rbind(rv$floor_list, rv$floor_json)
          }
        }
      }
      
      for (i in seq(from = as.POSIXct(input$start_date, format = "%Y-%m-%d"), to = as.POSIXct(input$end_date, format = "%Y-%m-%d"), by = "day")) {
        rv$elevation_response <- GET(url = paste0("https://api.fitbit.com/1/user/-/activities/elevation/date/", strftime(as.POSIXct(i, origin = "1970-01-01"), format = "%Y-%m-%d"), "/1d/1min.json"), add_headers(Authorization = rv$auth_code)) %>%
          content("parsed")
        if(length(rv$elevation_response == 1)){
          break()
        } else{
          if(length(rv$elevation_response$`activities-elevation-intraday`$dataset) == 0){
            next()
          } else{
            rv$elevation_json <- rv$floor_response$`activities-elevation-intraday`$dataset %>% 
              bind_rows() %>%
              mutate(time = paste(strftime(as.POSIXct(i, origin = "1970-01-01"), format = "%Y-%m-%d"), time))
            rv$elevation_list <- rbind(rv$elevation_list, rv$elevation_json)
          }
        }
      }
  })

This may be overwhelming but it is a lot of repetition (unfortunately!). Firstly, we run the get_active_minutes function (shown below) which collects daily summary data for sedentary, light, fairly and very active minutes.

The rest of the code above is for loops that collect intraday data for heart rate, steps, calories, distance, floors climb and elevation. They all follow the same structure we discussed in earlier sections of this post regarding how to extract intraday heart rate data with changes to the GET url in each loop to access different variables in the Fitbit API. The only subtle difference is in the last two for loops (floors climbed and elevation). These loops contain an extra step of logic where if the length of the response from the Fitbit API is equal to 1 we use the break command to end the loop. This is due to some Fitbit models being unable to measure these features and therefore an error is returned by the API. This step catches this error and prevent the rest of the code running which stops the app terminating.

The final section of code produces the necessary outputs. To do this we use the renderDataTable function and provide it with the dataset we want it to render. YOu may also notice we use the rename function to change the column names of variables to be more meaningful.

  output$daily_summary <- renderDataTable(rv$daily_summary)
  output$hr <- renderDataTable(rv$hr_list %>% bind_rows() %>% rename(date_time = time, heart_rate = value))
  output$step <- renderDataTable(rv$step_list %>% bind_rows() %>% rename(date_time = time, steps = value))
  output$calorie <- renderDataTable(rv$calorie_list %>% bind_rows() %>% rename(date_time = time, calories = value) %>% mutate(calories = round(calories, digits = 2)))
  output$distance <- renderDataTable(rv$distance_list %>% bind_rows() %>% rename(date_time = time, distance = value))
  output$floor <- renderDataTable(rv$floor_list %>% bind_rows() %>% rename(date_time = time, floors = value))
  output$elevation <- renderDataTable(rv$elevation_list %>% bind_rows() %>% rename(date_time = time, elevation = value))

The final piece of code is used to allow our data to be downloaded as a csv file using the download button we created in the side bar earlier. To do this, we use the downloadHandler function and provide it with our desired file name (users can change this it is just a place holder). We then use the content argument to specify we want to write the data to an excel workbook (using write_xlsx) and then provide a list of all the dataframes we want to write to the workbook.

output$download_data <- downloadHandler(
    filename = function() {
      "fitbit_data.xlsx"
    },
    content = function(file) {
      write_xlsx(list(rv$daily_summary, 
                      rv$hr_list %>% bind_rows() %>% rename(heart_rate = value),
                      rv$step_list %>% bind_rows() %>% rename(step = value),
                      rv$calorie_list %>% bind_rows() %>% rename(calorie = value) %>% mutate(calorie = round(calorie, digits = 2)),
                      rv$distance_list %>% bind_rows() %>% rename(distance = value),
                      rv$floor_list %>% bind_rows(),
                      rv$elevation_list %>% bind_rows()), path = file)
    }
  )

And that’s it!! We have a fully functioning app that allows Fitbit data to be retrieved using the Web API.

The App

The full app is included below for you to experiment with!

Expanding to collecting data from more than one participant

If you are looking to collect data from multiple participants, my advice is not to use the code above! Instead, I would recommend investigating the authorisation code grant flow. Using this methodology, you can create a single application that all participants consent to using their data. Then pull the data from here.

If you really want to to use the code above for multiple participants, it is theoretically relatively simple. It is not something I have personally tried if/when I do I will add the code below. My current thought process if to write an overaching function, collect_fitbit_data (or similar), which accepts a list of authorisation codes. Then, the function runs through all the functions above to collect the data relating to one participant using the first authorisation code. Next, this data is saved into a list (this may need to be a list of dataframes or a list of lists of dataframes) before the function iterates onto the next authorisation code to collect the remaining participants data.

Whilst this should be relatively simple to write, there is one big challenge you may have spotted - what happens if we get errors from the API? The code would need to be highly robust to errors as well as missing data which presents a coding challenge. There are several ways to handle this problem but all of them would add complexity to already complex code. Therefore as mentioned above, for larger numbers of participants another approach may be warranted.

Conclusion

The Fitbit Web API allows an extensive amount of data to be retrieved however a good level of technical understanding is required to utilise it effectively. Luckily, we can create a Shiny app to simplify this process for users not well reversed in programming. Hopefully both the app, and this explanation, was helpful to you and if there are any features you would like added to the application please get in touch.