Export Google Fit Daily Steps, Weight and Distance to a Google Sheet

Updated on Friday, December 29, 2023

Comments Page 19

Return to post.



Thanks for the quick reply Robert, really appreciate it. Problem was magically solved over night - I am assuming this may have something to do with oauth verification that can take a bit of time.

Again, thanks for this amazing article and your help!

Robert Ellison

Hi Matthias, that looks like a problem with the Google Fit API configuration. Check that you have copied the ClientID and ClientSecret over to the script correctly. Also go to the Google API Console and check that the Google Fit API is enabled.


This is amazing - Thanks Robert!

I have done all the steps you have outlined but get a 403 error message, containing "access not configured". The error messages reads:

"Request failed for {URL from Line 56 - which is th Fetch App URL} returned code 403. Truncated server response: { "error": { "errors": [ { "domain": "usageLimits", "reason": "accessNotConfigured", "message": "Access Not Configured. Fitness h... (use muteHttpExceptions option to examine full response).

The authentication came back with a success so not sure why is causing the missing access. Any idea how to solve this?




Hi Robert - thanks for the reply. The app never registered with Fit, so I couldn't deauthorize there. Did try the reset settings. I've moved on to doing it in Python (easier data munging for me), but your post was quite helpful in learning the basics. Thanks!

Robert Ellison

John, sorry for the slow reply. Have you tried disconnecting from Google Fit and then authorizing again? See the instructions near the end of the post. I'd also try running the Reset Settings menu option in the script as well before attempting to connect again.


Hi - Thanks much for the howto. I'm getting hung up at the authorization stage. It goes through such that I see I have given access (in the permissions page of my account) for the application to Google Docs, but NOT for Google Fit. Every time I try to do anything off the menu, it repeats the access process.

Any advice? Could the problem be trying to give the script access to fit data on a different google account than the spreadsheet?

Thanks for any help or pointers!


Hi Robert.

This is exactly what I was trying to figure out. I stopped and googled - thanks man, this is great!

Sachin Jain


Below bit of code fetches based on Month & activity & writes to the history tab the Start Date, End Date, Activity Name & Distance in kms.

It needs a sheet a called ActivityType which is basically a copy paste of all google fitactivity types.

function onOpen() {

var ui = SpreadsheetApp.getUi();

ui.createMenu('Google Fit')

.addItem('Authorize if needed (does nothing if already authorized)', 'showSidebar')

.addItem('Get Metrics Month Wise By Activity', 'getMetricsMonthWise')

.addItem('Reset Settings', 'clearProps')



function getMetricsMonthWise(){

var today = new Date();

for(var month=0; month <= today.getMonth(); month++){

var startDate = new Date();




var endDate = new Date(startDate.getYear(),month + 1, 0 );


getMetricsForDays(startDate, endDate, 'History');



function getMetricsForDays(start, end, tabName) {

Logger.log('Start Date' + start);

Logger.log('End Date' + end);

var fitService = getFitService();

var request = {

"aggregateBy": [


"dataTypeName": "com.google.distance.delta",

"dataSourceId": "derived:com.google.distance.delta:com.google.android.gms:merge_distance_delta"



"bucketByActivityType": { "minDurationMillis": 60000},

"startTimeMillis": start.getTime(),

"endTimeMillis": end.getTime()


var response = UrlFetchApp.fetch('URL', {

headers: {

Authorization: 'Bearer ' + fitService.getAccessToken()


'method' : 'post',

'contentType' : 'application/json',

'payload' : JSON.stringify(request, null, 2)


var json = JSON.parse(response.getContentText());

var ss = SpreadsheetApp.getActiveSpreadsheet();

var sheet = ss.getSheetByName(tabName);

for(var b = 0; b < json.bucket.length; b++) {

var distance = -1;

if (json.bucket[b].dataset[0].point.length > 0) {

distance = json.bucket[b].dataset[0].point[0].value[0].fpVal/1000;


var activity = getActivityName(json.bucket[b].activity);

sheet.appendRow([start, end,


distance == -1 ? ' ' : distance]);



function getActivityName(activityCode) {

var ss = SpreadsheetApp.getActiveSpreadsheet();

var sheet = ss.getSheetByName('ActivityType');

var startColumn = 2;

var startRow = 1;

for (var row_num = startRow; row_num < 120; row_num++) {

var cellValue = sheet.getRange(row_num, startColumn).getValue();

if (cellValue == activityCode) {

var activityName = sheet.getRange(row_num, startColumn-1).getValue();

return activityName;



return 'INVALID CODE:' + activityCode;



Maximal amount of days to request is 90

I'll add a user promt to enter amount of days to load, and then load it all by 90-day chunks.

function showPrompt() {

var ui = SpreadsheetApp.getUi(); // Same variations.

var result = ui.prompt(

'Let\'s get old days!',

'Please enter number of days:',


// Process the user's response.

var button = result.getSelectedButton();

var text = result.getResponseText();

if (button == ui.Button.OK) {

// User clicked "OK".

n = parseInt(text)

if (n <= 90)

getMetricsForDays(1, n, 'History');

if (n > 90)



if (n-90<0)

getMetricsForDays(1, n, 'History');


getMetricsForDays(n-89, n, 'History');





Sachin Jain

Thanks. That did the trick :).

I am still working on the code. Will post it here once it is finished.

Add Comment

All comments are moderated. Your email address is used to display a Gravatar and optionally for notification of new comments and to sign up for the newsletter.