Jarkko Tervonen

How to Download All Activities from Garmin Connect in 2026

· Jarkko

Back in 2023, I wrote a blog post about downloading all your activities from Garmin Connect using a browser console script. The old script stopped working — Garmin changed their API endpoints and added CSRF token requirements. So here’s an updated version that works with the current Garmin Connect in 2026.

What changed?

The 2023 script relied on older Garmin Connect API endpoints that have since been deprecated. The main differences in the 2026 version:

Failed download reporting. At the end of the run, the script logs a table of any activities that couldn’t be downloaded, including the activity ID, name, date, and error code. This makes it easy to retry specific activities manually.

What the script does
The script runs directly in your browser and does two things:

  1. Fetches your full activity list by paginating through the Garmin Connect API, 20 activities at a time.
  2. Downloads each activity as a TCX file (or GPX — you can change the format) and saves it to your default downloads folder.

It uses your existing Garmin Connect session, so there’s no need to deal with API keys or OAuth tokens. You just need to be logged in.

How to use it

  1. Open Garmin Connect
  2. Log in to your account
  3. Open the All Activities page
  4. Open browser Developer Tools (F12)
  5. Open the Console tab Paste the script below and press Enter

The script will start fetching your activity list and then download each activity one by one. Progress is logged to the console. Depending on how many activities you have, this can take a while — the delays between requests are intentional to avoid getting rate-limited by Garmin.

Tip: If you want GPX files instead of TCX, change the type variable on the first line from 'tcx' to 'gpx'.

Note: Your browser may ask you to allow multiple downloads. Click “Allow” when prompted, otherwise the script will stall.

The script

const type = 'tcx'; // change to 'gpx' if needed
const pageSize = 20;
const maxActivities = 10000;
const listDelay = 500;
const baseDownloadDelay = 2000;
const maxRetries = 4;
const maxCooldown = 30000;

const sleep = ms => new Promise(r => setTimeout(r, ms));

const csrf =
  document.querySelector('meta[name="csrf-token"]')?.content ||
  window.Garmin?.csrfToken ||
  localStorage.getItem('csrfToken') ||
  sessionStorage.getItem('csrfToken');

const headers = {
  'accept': '*/*',
  'cache-control': 'no-cache',
  'pragma': 'no-cache',
  'x-app-ver': '5.24.1.1',
  ...(csrf ? { 'connect-csrf-token': csrf } : {}),
};

async function getAllActivities() {
  const activities = [];
  for (let start = 0; start < maxActivities; start += pageSize) {
    const url =
      `https://connect.garmin.com/gc-api/activitylist-service/activities/search/activities?limit=${pageSize}&start=${start}`;
    const r = await fetch(url, {
      credentials: 'include',
      headers
    });
    if (!r.ok) break;
    const page = await r.json();
    if (!Array.isArray(page) || page.length === 0) break;
    activities.push(...page);
    console.log(`Fetched ${activities.length}`);
    if (page.length < pageSize) break;
    await sleep(listDelay);
  }
  return activities;
}

async function saveBlob(blob, filename) {
  const a = document.createElement('a');
  a.href = URL.createObjectURL(blob);
  a.download = filename;
  document.body.appendChild(a);
  a.click();
  URL.revokeObjectURL(a.href);
  a.remove();
}

async function downloadActivity(activity) {
  const id = activity.activityId;
  let cooldown = baseDownloadDelay;
  let lastError = '';

  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      const r = await fetch(
        `https://connect.garmin.com/gc-api/download-service/export/${type}/activity/${id}`,
        {
          credentials: 'include',
          headers
        }
      );

      if (r.ok) {
        const blob = await r.blob();
        await saveBlob(blob, `activity_${id}.${type}`);
        console.log(`Downloaded ${id}`);
        return { ok: true };
      }

      lastError = `${r.status}`;
      if (r.status === 403 || r.status === 429 || r.status >= 500) {
        cooldown = Math.min(cooldown * 2, maxCooldown);
      }
    } catch (e) {
      lastError = 'network';
      cooldown = Math.min(cooldown * 2, maxCooldown);
    }

    await sleep(cooldown);
  }

  return {
    ok: false,
    id,
    name: activity.activityName || '',
    date: activity.startTimeLocal || '',
    error: lastError
  };
}

(async () => {
  const activities = await getAllActivities();
  const failed = [];
  console.log(`Found ${activities.length} activities`);

  for (const activity of activities) {
    await sleep(baseDownloadDelay);
    const result = await downloadActivity(activity);
    if (!result.ok) failed.push(result);
  }

  console.log('Finished');
  if (failed.length) {
    console.table(failed);
    console.log(
      'Failed IDs:\n' +
      failed.map(f => f.id).join('\n')
    );
  } else {
    console.log('All downloads succeeded');
  }
})();

Note: This blog post was AI-generated based on the code.

#data   #garmin