Skip to main content

gc2everywhere — Garmin Connect to Everywhere

Keep a copy of your Garmin workouts. Send them to the services you choose.

This Python tool uses Garmin's official Personal API to download original activity files, save them in a folder you control, and send new recordings to Intervals.icu and Effort Signal. A local Dropbox folder works like any other folder. No Garmin password or browser automation is required.

Why this exists

Independent services have built useful, sometimes unique features that Garmin does not offer. They have helped people get more from their devices and given them more reasons to buy Garmin products.

Garmin's move toward paid commercial API access makes life harder for those services. Its current program distinguishes a limited Business Starter tier from Enterprise access with negotiated commercial terms.

We think it is disappointing that Garmin has chosen a direction that puts more of the burden on ordinary users, after independent developers helped make its products more valuable. People should not have to become integration engineers to keep using their own workout data. This small tool is our practical response: keep your files, choose your services, and keep moving.

This is an independent project, not an official Garmin product.

Disclaimer: Provided "as is", without warranties, under the MIT License. To the extent permitted by applicable law, the authors and copyright holders are not liable for losses or damages arising from its use, including data loss, failed or duplicate transfers, account restrictions, or service charges. You are responsible for backups, API keys, and complying with each service's terms.

Install

You need Python 3.11 or newer. Install from PyPI in a virtual environment:

On macOS or Linux:

python3 -m venv .venv
source .venv/bin/activate
python -m pip install gc2everywhere

On Windows, in PowerShell:

py -m venv .venv
.venv\Scripts\python.exe -m pip install gc2everywhere

The examples use gc2e. On Windows, use .venv\Scripts\gc2e.exe instead. On macOS/Linux, activate .venv again in a new terminal or use the full path to .venv/bin/gc2e.

To install from a source checkout instead, run python -m pip install . from the repository directory with the virtual environment activated.

Get your keys

Garmin: open Personal API settings in Garmin Connect, create a Personal API key, choose its expiry, and copy it when shown. This is not an OAuth application client secret. Garmin offers expiry choices of 30, 90, 180 days or one year. See the Personal API FAQ.

Intervals.icu: sign in, open Settings, scroll to Developer Settings, and create an API key. No athlete ID is needed: the tool uses the account belonging to the key. See the maintainer's API access instructions.

Effort Signal: open Settings → Integrations → Personal API keys, select Create API key, give it a name such as gc2everywhere, allow activities:write, and choose an expiry. Copy the key immediately, it is shown once. Other data-write permissions are unnecessary for workout files. See the Personal API key guide for creation, permissions, usage, expiry, and revocation.

First run

On macOS or Linux, paste your keys into these environment variables. Omit a destination key if you do not want to send files to that service.

export GARMIN_API_KEY='your-garmin-personal-api-key'
export INTERVALS_API_KEY='your-intervals-api-key'
export EFFORTSIGNAL_API_KEY='your-effortsignal-personal-api-key'

gc2e sync --archive-dir "$HOME/Dropbox/Garmin"

On Windows PowerShell:

$env:GARMIN_API_KEY = 'your-garmin-personal-api-key'
$env:INTERVALS_API_KEY = 'your-intervals-api-key'
$env:EFFORTSIGNAL_API_KEY = 'your-effortsignal-personal-api-key'
.venv\Scripts\gc2e.exe sync --archive-dir "$HOME\Dropbox\Garmin"

The folder is created if needed. There are no setup questions. The first sync checks the last seven days, downloads recordings, and sends them to the services whose keys you supplied. Later runs catch up from the last completed scan with a seven-day overlap, so recent late uploads are found too. For older workouts, use import-history.

For archive only, omit destination keys or add --archive-only.

Keys pasted into a terminal may remain in shell history. For regular use, keep the exports in a private file outside Dropbox and this repository, restrict its permissions (chmod 600 /path/to/private-keys.sh), and load it with source /path/to/private-keys.sh. Never share keys in arguments, logs, screenshots or scripts. Revoke and replace an exposed key.

Four commands

Command What it does
sync Downloads new recordings, sends them to configured services, and retries earlier deliveries.
import-history Downloads past recordings. Never sends them automatically.
send Explicitly sends saved, undelivered recordings. No Garmin key needed.
status Shows local progress and errors. Makes no network requests.

Save your history first:

gc2e import-history --from 2020-01-01 --archive-dir "$HOME/Dropbox/Garmin"

Send the saved history to one service when you are ready:

gc2e send --destination intervals --archive-dir "$HOME/Dropbox/Garmin"

Or select a date range:

gc2e send --destination effortsignal --from 2026-01-01 --to 2026-10-21 \
  --archive-dir "$HOME/Dropbox/Garmin"

Dates are inclusive and use the workout's local start date in Garmin. The history available to your key is controlled by Garmin; a requested range cannot override its access restrictions. --destination can be repeated.

Adding a destination key later does not send the existing archive to it. Use send for that. Workouts discovered during --archive-only also need an explicit send. A key for a different account creates a different delivery identity; use it only if you intend to send files to that account.

Files, progress, and retries

Garmin/
  .gc2everywhere.json
  2026/10/
    123456789.fit
    123456789.zip
    123456789.json

The filename is the Garmin activity ID, not the workout name or a timestamp. The ZIP is the unchanged original; FIT is extracted for easy use elsewhere. JSON records the checksum and stable upload revision. A raw FIT response does not create a ZIP.

Complete existing files are reused. Interrupted downloads never become finished files. Repeated runs skip completed downloads and deliveries. A changed or corrupt local file produces an error instead of silently replacing it. Manual activities without a downloadable FIT are logged as skipped. ZIPs containing several FIT files are not treated as one workout.

The delivery journal and Garmin's temporary access token live outside Dropbox, under ~/.local/state/gc2everywhere/ by default (XDG_STATE_HOME is honored). --state-dir overrides that location. Keep one writer per archive: a local lease prevents overlapping runs using the same journal, but cannot coordinate different computers writing to Dropbox.

After moving an archive or losing the journal, sidecars restore the inventory without downloading files again. Previous service receipts cannot be recovered locally, so restored history stays unsent until you run send. Receivers also check duplicates; exactly-once delivery cannot be guaranteed if a connection fails after the server accepts an upload.

Effort Signal's accepted means queued. Later runs poll its receipt and mark it sent only after publication. failed and needs_review require inspection in Effort Signal. Failure in one destination does not stop the others. Rate limits honor Retry-After; retryable upload errors use increasing delays across runs.

This is a recording synchronizer, not a mirror of all Garmin data. It does not sync wellness metrics, deletions, subsequent workout edits, or manually changed files.

Logs and scheduling

Logs include activity IDs, discovery, filenames, sizes, skips, each destination's outcome, retries, and a final count. They exclude keys, authorization headers, raw API responses, and FIT contents. IDs and dates are still personal data.

gc2e sync --archive-dir "$HOME/Dropbox/Garmin" --log-file "$HOME/gc2everywhere.log"
gc2e status --archive-dir "$HOME/Dropbox/Garmin"

Logs rotate at 2 MiB with three backups. --verbose adds HTTP request counts. --json prints a machine-readable summary to stdout; logs use stderr. Exit codes: 0 successful run, 1 operational error, 2 invalid arguments, 130 interrupted.

For a large history, --max-activities 50 bounds each download and destination batch. Repeat the command to continue; completed work is retained.

Run sync every 30 minutes with your computer's scheduler. On macOS/Linux, create a private script using absolute paths:

#!/bin/sh
. /absolute/path/to/private-keys.sh
exec /absolute/path/to/gc2everywhere/.venv/bin/gc2e sync \
  --archive-dir /absolute/path/to/Dropbox/Garmin \
  --log-file /absolute/path/to/gc2everywhere.log

Add */30 * * * * /bin/sh /absolute/path/to/sync-garmin.sh with crontab -e. On Windows, use Task Scheduler for a private PowerShell script with the environment assignments and command above. A sleeping computer catches up on its next run.

Cloudflare Workers

Experimental: See CLOUDFLARE_WORKERS.md in the repository or the source distribution on PyPI. The Worker deployment files are included in that source archive; installing the CLI with pip does not deploy a Worker.

Contributing a destination

Pull requests adding other services are welcome. Destinations are independent modules under src/gc2everywhere/destinations/. Implement the Destination contract:

  1. Declare a name and the environment variable for its API key.
  2. connect() verifies access and sets a stable account identity.
  3. send(row, content, filename) uploads and returns a Delivery.
  4. For asynchronous processing, return accepted with a receipt and implement poll(receipt). Return sent only after confirmation.
  5. Register the class in DESTINATIONS, document key creation, and add mocked tests for request format, failures, retries, and duplicates.

Use the shared transport so request limits and safe diagnostics work locally and in Workers. Never log secrets or follow credential-bearing redirects. Document Worker limitations if a service does not accept ZIP; local delivery supplies FIT.

Developer checks, with no keys or live API calls:

uv sync --group dev
uv run pytest -q
uv run ruff check src tests
uv run ruff format --check src tests

Metadata

Release files for gc2everywhere 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for gc2everywhere 0.1.0
File Size Uploaded Provenance
gc2everywhere-0.1.0.tar.gz 68.4 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for gc2everywhere 0.1.0
File Interpreter ABI Platform
gc2everywhere-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 98.9 kB

Release files / gc2everywhere-0.1.0.tar.gz

Download URL gc2everywhere-0.1.0.tar.gz
Size 68.4 kB
Tags Source
SHA-256 checksum
How to use checksums
0344195e30374d542f95b7735b25dffa64d795c5b5ab78963fcb12032117b04b
BLAKE2b-256 checksum
How to use checksums
e4361214be294d871f6134270fee0ab33033185b1883e0831ca8416affd6e58d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.11.23 {"installer":{"name":"uv","version":"0.11.23","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release files / gc2everywhere-0.1.0-py3-none-any.whl

Download URL gc2everywhere-0.1.0-py3-none-any.whl
Size 30.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
31d768fa0a0a2452a0187858381bd200270d663cda8fafc89d9dd751fd55f86b
BLAKE2b-256 checksum
How to use checksums
b2d8dfa6618332cb83acd16cd07e94475e4afd4b95732792d97fdad48cf2ce6c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.11.23 {"installer":{"name":"uv","version":"0.11.23","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Security for gc2everywhere 0.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page