Create a gist now

Instantly share code, notes, and snippets.

What would you like to do?
bulk-export run data from Garmin Connect
#!/usr/bin/env ruby
## disconnect
# ./disconnect.rb -u yourusername
# This is a command-line utility for the bulk-downloading of run data from
# the web application, which has lackluster export
# capabilities.
# Using this code is a matter of your own relationship with Garmin Connect
# and their TOS. I can't imagine this being very destructive to their service,
# and it's just filling in a hole in their existing service.
# It's built against Garmin Connect as of July 22, 2011. It's a scraper:
# thus if Garmin changes, this **will break**.
# This script requires all of the utilities on the line below: install them
# with rubygems
%w{rubygems json fileutils mechanize choice highline/import}.map{|x| require x}
Choice.options do
header ''
header 'Specific options:'
option :user, :required => true do
short '-u'
long '--user=USER'
desc ' username. Required'
option :dir do
short '-o'
long '--output-dir=OUTPUT'
desc 'the directory to save .tcx files'
default 'tcx'
password = ask("Enter your password: " ) { |q| q.echo = "*" }
def login(agent, user, password)
page = agent.get(LOGIN_PAGE)
login_form = page.form('login')
login_form['login:loginUsernameField'] = user
login_form['login:password'] = password
page = agent.submit(login_form, login_form.buttons.first)
raise "Login incorrect!" if page.title().match('Sign In')
def download_run(agent, id)
print "."
# This downloads TCX files: you can swap out the constant, or add
# more lines that download the different kinds of exports. I prefer TCX,
# because despite being a 'private standard,' it includes all data,
# including heart rate data.
agent.get(TCX_EXPORT % (id).to_i).save_as(File.join(Choice[:dir], "%d.tcx" % id))
def activities(agent)
j = agent.get(ACTIVITIES_SEARCH)
search = JSON.parse(j.content)
runs = search['results']['activities'].map {|r|
# Get each activity id to insert into the download URL
}.map {|id|
# Download a run.
download_run(agent, id)
agent =
# One needs to log in to get access to private runs. Mechanize will store
# the session data for the API call that cames next.
home_page = login(agent, Choice[:user], password)
FileUtils.mkdir_p(Choice[:dir]) if not[:dir])
puts "Downloading runs..."

pza commented Aug 16, 2011

If you get 400 => Net::HTTPBadRequest, it is possibly because Garmin's servers appear to be limiting activity queries to 100 requests at a time. Modify start and limit in ACTIVITIES_SEARCH and try again.

Works perfectly! Thanks a lot for this. just used it to export a couple of years of cycling data from Garmin and import it into Strava.

lukszp commented Sep 11, 2011

Works perfectly! Thanks a lot!

@pza --> thanks for your comment!

Thanks for great inspiration on writing Garmin Connect backup script.

dhaskew commented Sep 24, 2011

yeah, thanks for this.

dano11 commented Oct 9, 2011

Ok. help me out here. Where and how do I copy and paste the above code? Thanks.

Thanks a lot for this, but.. I get Net::HTTPBadRequest even befgore I get one activity, and Garmin's "Activity Search Service 1.0" is deprecated: Is it over..?


tmcw commented Nov 18, 2011

@cloveras: try @gljeremy's fork:

@dano11 you'd copy & paste it into a file called disconnect.rb, and you'll need to gem install the dependencies on the top and run it with Ruby in a terminal.

Yes! Works perfectly, thanks a lot for the quick reply!

I note the TCX file downloaded does not contain the name or description associated with and activity on the garmin connect website, even though the file format supports that. :-( Can this script be extend to down loads as well?

rumland commented Apr 8, 2013

Yes, thanks a lot for sharing. I also experienced the 400 => Net::HTTPBadRequest problem. Instead of limiting ACTIVITIES_SEARCH to 100, I limited it to 1 and changed the activities function to look like the following:
def activities(agent)
$activityIdx = 0
$numActivities = 624
while $activityIdx <= $numActivities do
puts("Downloading activity = #$activityIdx")
as = ACTIVITIES_SEARCH % $activityIdx
print "\tas: ", as, "\n"
j = agent.get(as)
search = JSON.parse(j.content)
runs = search['results']['activities'].map {|r|
# Get each activity id to insert into the download URL
}.map {|id|
# Download an activity.
download_run(agent, id)
$activityIdx += 1

In my case I wanted all of my history, 624.

Thanks a lot for sharing @tmcw!

Garmin Connect UI has changed and this script no longer works for me.

magsol commented May 18, 2014

If you're interested in a Python solution, I have one that does pretty much the same thing (in fact, this script was inspiration for it) and also contains a fix for the new Garmin Connect UI (basically just needs a redirect after login):

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment