The Garmin Connect API library comes with two examples:
example.py - Simple getting-started example showing authentication, token storage, and basic API callsdemo.py - Comprehensive demo providing access to 130+ API methods organized into 13 categories for easy navigation$ ./demo.py
π Exported data will be saved to the directory: 'your_data'
π All API responses are written to: 'response.json'
Attempting to login using stored tokens from: ~/.garminconnect
Successfully logged in using stored tokens!
π Your Stats Today: 4,045 steps | 1445.0 kcal
π Time to get those legs moving!
==================================================
π΄ Full-blown Garmin Connect API Demo - Main Menu
==================================================
Select a category:
[1] π€ User & Profile
[2] π Daily Health & Activity
[3] π¬ Advanced Health Metrics
[4] π Historical Data & Trends
[5] π Activities & Workouts
[6] βοΈ Body Composition & Weight
[7] π Goals & Achievements
[8] β Device & Technical
[9] π½ Gear & Equipment
[0] π§ Hydration & Wellness
[a] π§ System & Export
[b] π
Training Plans
[c] β³ Golf
[d] βοΈ Activity Editing
[q] Exit program
Make your selection:
A comprehensive Python3 API wrapper for Garmin Connect, providing access to health, fitness, and device data.
This library enables developers to programmatically access Garmin Connect data including:
Compatible with all Garmin Connect accounts. See https://connect.garmin.com/
Requires Python 3.12 or later.
Install from PyPI:
pip install --upgrade garminconnect curl_cffi
Clone the repo, then:
python3 -m venv .venv --copies
source .venv/bin/activate # On Windows: .venv\Scripts\activate
pip install -e ".[example]"
python3 ./example.py # simple getting-started example
python3 ./demo.py # comprehensive demo (130+ API methods)
This project uses PDM for dependency management and task automation.
β οΈ Important: Create a virtual environment first on externally-managed Python installs (Debian/Ubuntu) to avoid system package conflicts.
python3 -m venv .venv --copies
source .venv/bin/activate # On Windows: .venv\Scripts\activate
pip install pdm
python3 -m pdm install --group :all
pre-commit install --install-hooks # optional but recommended
Note: Using
python -m pdminstead ofpdmavoids PATH issues on some Windows setups wherepip install pdmplaces thepdmexecutable outside the directories onPATH. Oncepdm installhas run, subsequentpdm run ...commands work normally because the venv'sScripts/directory is onPATHwhile the venv is active.
Development commands:
pdm run format # Auto-format code (isort, black, ruff --fix)
pdm run lint # Check code quality (isort, ruff, black, mypy)
pdm run codespell # Check spelling
pdm run test # Run test suite
pdm run testcov # Run tests with coverage report
pdm run all # Run all checks (lint + codespell + pre-commit + test)
pdm run clean # Clean build artifacts and cache files
pdm run build # Build package for distribution
pdm run publish # Build and publish to PyPI
pdm run --list # Show all available commands
Run pdm run format && pdm run lint && pdm run test before submitting PRs.
Authentication uses the same mobile SSO flow as the official Garmin Connect Android app. No browser is needed.
How it works:
sso.garmin.com/mobile/api/login using the Android
app's client ID. If MFA is required, a callback (prompt_mfa) prompts for the one-time code.access_token + refresh_token) via diauth.garmin.com. Tokens are stored at
~/.garminconnect/garmin_tokens.json.Session lifetime:
Token storage:
~/.garminconnect/garmin_tokens.json # saved automatically, mode 0600
The containing directory is restricted to mode 0700. Treat the token file
like a password: the refresh token can provide persistent account access. Avoid
putting a Garmin password in shell history or a long-lived environment variable;
prefer getpass() or another interactive secret prompt.
Resilient login (multi-strategy + token validation):
login() tries several authentication strategies in order (mobile, SSO widget,
web portal β each with and without TLS impersonation) and only declares success
when the resulting token is actually accepted by the API. If a strategy obtains
a token the API later rejects (a region/account-specific condition β see
#369), the
library transparently falls through to the next strategy. Set
Garmin(..., verify_login=False) to restore the legacy "first token wins"
behavior.
Cached-token gotcha & self-healing: when a tokenstore is supplied,
login() loads those tokens before the strategy chain and short-circuits if
they load β so stale/poisoned cached tokens used to fail every run. The library
now detects this: if cached tokens are rejected by the API, it discards them and
performs a fresh credential login automatically. To force a clean slate yourself
(e.g. between a failed resume and a retry), call:
g.logout() # clears in-memory auth + cached tokens (uses GARMINTOKENS)
g.logout(tokenstore) # or pass an explicit path
logout() removes only the local garmin_tokens.json file and preserves its
directory and unrelated files. It does not revoke a token that has already
been issued by Garmin. Revoke account access from Garmin's account/security
settings if a token may have been copied or exposed.
This is an unofficial client for Garmin's web services; it does not pair with
the Garmin Connect phone app or connect directly to a watch. When you call
login(), your credentials and MFA code are sent over HTTPS to Garmin's login
service. The library receives an access/refresh token pair and, when a token
store is supplied, caches it locally for later sessions. Subsequent API methods
send that token to Garmin and can read or change the same account data that the
selected method targets.
The library does not automatically download an entire account. The demo writes
responses, activity downloads, and health reports only when you select those
actions. Demo exports are stored under your_data/ with owner-only directory
and file permissions. Run the project in a dedicated virtual environment, read
the method you plan to call, and start with read-only methods. Upload, edit,
delete, schedule, hydration, and weigh-in methods can change Garmin account data.
The default suite is credential-free and excludes live-account integration tests:
pdm run test # Run all tests
pdm run testcov # Run tests with coverage report
To explicitly run live integration tests, use a test account if possible. This can create local VCR recordings and includes methods that may mutate the account:
export GARMIN_EMAIL="you@example.com"
read -s GARMIN_PASSWORD && export GARMIN_PASSWORD
pdm run pytest -m integration --vcr-record=once
unset GARMIN_PASSWORD
VCR recordings are ignored by Git because Garmin responses contain sensitive health, activity, location, and account data. Do not commit them.
For package maintainers:
Setup PyPI credentials:
pip install twine
# Edit with your preferred editor, or create via here-doc:
# cat > ~/.pypirc <<'EOF'
# [pypi]
# username = __token__
# password = <PyPI_API_TOKEN>
# EOF
[pypi]
username = __token__
password = <PyPI_API_TOKEN>
Recommended: use environment variables and restrict file perms
chmod 600 ~/.pypirc
export TWINE_USERNAME="__token__"
export TWINE_PASSWORD="<PyPI_API_TOKEN>"
Publish new version:
pdm run publish # Build and publish to PyPI
Alternative publishing steps:
pdm run build # Build package only
pdm publish # Publish pre-built package
We welcome contributions! Here's how you can help:
Before contributing:
pdm run format && pdm run lintpdm run testExplore the API interactively with our reference notebook.
import os
from getpass import getpass
from datetime import date
from garminconnect import Garmin
# First run: logs in and saves tokens to ~/.garminconnect
# Subsequent runs: loads saved tokens and auto-refreshes
client = Garmin(
os.getenv("GARMIN_EMAIL"),
getpass("Garmin password: "),
prompt_mfa=lambda: input("MFA code: "),
)
client.login("~/.garminconnect")
# Get today's stats
today = date.today().isoformat()
stats = client.get_stats(today)
# Get heart rate data
hr_data = client.get_heart_rates(today)
print(f"Resting HR: {hr_data.get('restingHeartRate', 'n/a')}")
The library includes optional typed workout models for creating type-safe workout definitions:
pip install garminconnect[workout]
from garminconnect.workout import (
RunningWorkout,
WorkoutSegment,
create_warmup_step,
create_interval_step,
create_distance_interval_step,
create_cooldown_step,
create_repeat_group,
)
# Create a structured running workout
workout = RunningWorkout(
workoutName="Easy Run",
estimatedDurationInSecs=1800,
workoutSegments=[
WorkoutSegment(
segmentOrder=1,
sportType={"sportTypeId": 1, "sportTypeKey": "running"},
workoutSteps=[create_warmup_step(300.0)],
)
],
)
# Upload and optionally schedule it
result = client.upload_running_workout(workout)
client.schedule_workout(result["workoutId"], "2026-03-20")
# Edit it in place - keeps its id, so any schedules pointing at it stay valid
workout_data = client.get_workout_by_id(result["workoutId"])
workout_data["workoutName"] = "Easy Run (revised)"
client.update_workout(result["workoutId"], workout_data)
# Delete a workout or remove it from the calendar
client.delete_workout(workout_id)
client.unschedule_workout(scheduled_workout_id)
# Push a workout to a device - defaults to the last workout / last used device
client.push_workout_to_device(result["workoutId"], device_id)
Available workout classes: RunningWorkout, CyclingWorkout, SwimmingWorkout, WalkingWorkout, HikingWorkout, StrengthWorkout, MultiSportWorkout, FitnessEquipmentWorkout
Strength workouts are rep-based. Build each exercise with create_strength_set(category, step_order, sets, reps, rest_seconds, exercise_name="", weight_kg=None) and identify exercises with a category / exercise pair from the bundled catalog in garminconnect.exercises (1,527 exercises across 47 categories, with resolve(name) and find(term) helpers):
from garminconnect import exercises
from garminconnect.workout import StrengthWorkout, WorkoutSegment, create_strength_set
lat = exercises.resolve(
"Lat Pull-down"
) # {'category': 'PULL_UP', 'exercise': 'LAT_PULLDOWN'}
workout = StrengthWorkout(
workoutName="Upper Body",
estimatedDurationInSecs=0,
workoutSegments=[
WorkoutSegment(
segmentOrder=1,
sportType={"sportTypeId": 5, "sportTypeKey": "strength_training"},
workoutSteps=[
create_strength_set(
"BENCH_PRESS", step_order=1, sets=4, reps=10, rest_seconds=120
),
create_strength_set(
lat["category"],
step_order=4,
sets=3,
reps=12,
rest_seconds=90,
exercise_name=lat["exercise"],
),
],
)
],
)
client.upload_strength_workout(workout)
Helper functions: create_warmup_step, create_interval_step, create_distance_interval_step, create_recovery_step, create_cooldown_step, create_repeat_group, create_strength_exercise_step, create_strength_rest_step, create_strength_set
Use create_distance_interval_step(600.0, step_order=1) for interval steps that should end after a distance in meters instead of after a duration.
The library supports reading Garmin Golf scorecards, shot-level data, club statistics, and golf user statistics.
# List recent golf scorecards
summary = client.get_golf_summary(limit=10)
scorecard_id = summary["scorecardSummaries"][0]["id"]
# Get the full scorecard details
scorecard = client.get_golf_scorecard(scorecard_id)
# Get shot-by-shot data
# - Omit hole_numbers to receive every hole (recommended for holes 10-18).
# - Single-digit holes work as a comma- or dash-separated list, e.g. "1,2,3".
# Commas are normalized to dashes because Garmin only accepts '-' as the
# separator.
# - Double-digit holes (10-18) cannot be fetched with a filter at all; the
# API either drops them or returns an empty response. If you request holes
# 10-18, the library falls back to returning all 18 holes.
shots = client.get_golf_shot_data(scorecard_id)
shots = client.get_golf_shot_data(scorecard_id, hole_numbers="1,2,3")
# Club and player statistics
club_stats = client.get_golf_club_stats()
user_stats = client.get_golf_user_stats()
tests/ directorySpecial thanks to all contributors who have helped improve this project:
This project thrives thanks to community involvement and feedback.
If you find this library useful for your projects, please consider supporting its continued development and maintenance:
Why Support?
Every contribution, no matter the size, makes a difference and is greatly appreciated! π
(top 30 of 77)
Python
100.0%
The Garmin Connect API library comes with two examples:
example.py - Simple getting-started example showing authentication, token storage, and basic API callsdemo.py - Comprehensive demo providing access to 130+ API methods organized into 13 categories for easy navigation$ ./demo.py
π Exported data will be saved to the directory: 'your_data'
π All API responses are written to: 'response.json'
Attempting to login using stored tokens from: ~/.garminconnect
Successfully logged in using stored tokens!
π Your Stats Today: 4,045 steps | 1445.0 kcal
π Time to get those legs moving!
==================================================
π΄ Full-blown Garmin Connect API Demo - Main Menu
==================================================
Select a category:
[1] π€ User & Profile
[2] π Daily Health & Activity
[3] π¬ Advanced Health Metrics
[4] π Historical Data & Trends
[5] π Activities & Workouts
[6] βοΈ Body Composition & Weight
[7] π Goals & Achievements
[8] β Device & Technical
[9] π½ Gear & Equipment
[0] π§ Hydration & Wellness
[a] π§ System & Export
[b] π
Training Plans
[c] β³ Golf
[d] βοΈ Activity Editing
[q] Exit program
Make your selection:
A comprehensive Python3 API wrapper for Garmin Connect, providing access to health, fitness, and device data.
This library enables developers to programmatically access Garmin Connect data including:
Compatible with all Garmin Connect accounts. See https://connect.garmin.com/
Requires Python 3.12 or later.
Install from PyPI:
pip install --upgrade garminconnect curl_cffi
Clone the repo, then:
python3 -m venv .venv --copies
source .venv/bin/activate # On Windows: .venv\Scripts\activate
pip install -e ".[example]"
python3 ./example.py # simple getting-started example
python3 ./demo.py # comprehensive demo (130+ API methods)
This project uses PDM for dependency management and task automation.
β οΈ Important: Create a virtual environment first on externally-managed Python installs (Debian/Ubuntu) to avoid system package conflicts.
python3 -m venv .venv --copies
source .venv/bin/activate # On Windows: .venv\Scripts\activate
pip install pdm
python3 -m pdm install --group :all
pre-commit install --install-hooks # optional but recommended
Note: Using
python -m pdminstead ofpdmavoids PATH issues on some Windows setups wherepip install pdmplaces thepdmexecutable outside the directories onPATH. Oncepdm installhas run, subsequentpdm run ...commands work normally because the venv'sScripts/directory is onPATHwhile the venv is active.
Development commands:
pdm run format # Auto-format code (isort, black, ruff --fix)
pdm run lint # Check code quality (isort, ruff, black, mypy)
pdm run codespell # Check spelling
pdm run test # Run test suite
pdm run testcov # Run tests with coverage report
pdm run all # Run all checks (lint + codespell + pre-commit + test)
pdm run clean # Clean build artifacts and cache files
pdm run build # Build package for distribution
pdm run publish # Build and publish to PyPI
pdm run --list # Show all available commands
Run pdm run format && pdm run lint && pdm run test before submitting PRs.
Authentication uses the same mobile SSO flow as the official Garmin Connect Android app. No browser is needed.
How it works:
sso.garmin.com/mobile/api/login using the Android
app's client ID. If MFA is required, a callback (prompt_mfa) prompts for the one-time code.access_token + refresh_token) via diauth.garmin.com. Tokens are stored at
~/.garminconnect/garmin_tokens.json.Session lifetime:
Token storage:
~/.garminconnect/garmin_tokens.json # saved automatically, mode 0600
The containing directory is restricted to mode 0700. Treat the token file
like a password: the refresh token can provide persistent account access. Avoid
putting a Garmin password in shell history or a long-lived environment variable;
prefer getpass() or another interactive secret prompt.
Resilient login (multi-strategy + token validation):
login() tries several authentication strategies in order (mobile, SSO widget,
web portal β each with and without TLS impersonation) and only declares success
when the resulting token is actually accepted by the API. If a strategy obtains
a token the API later rejects (a region/account-specific condition β see
#369), the
library transparently falls through to the next strategy. Set
Garmin(..., verify_login=False) to restore the legacy "first token wins"
behavior.
Cached-token gotcha & self-healing: when a tokenstore is supplied,
login() loads those tokens before the strategy chain and short-circuits if
they load β so stale/poisoned cached tokens used to fail every run. The library
now detects this: if cached tokens are rejected by the API, it discards them and
performs a fresh credential login automatically. To force a clean slate yourself
(e.g. between a failed resume and a retry), call:
g.logout() # clears in-memory auth + cached tokens (uses GARMINTOKENS)
g.logout(tokenstore) # or pass an explicit path
logout() removes only the local garmin_tokens.json file and preserves its
directory and unrelated files. It does not revoke a token that has already
been issued by Garmin. Revoke account access from Garmin's account/security
settings if a token may have been copied or exposed.
This is an unofficial client for Garmin's web services; it does not pair with
the Garmin Connect phone app or connect directly to a watch. When you call
login(), your credentials and MFA code are sent over HTTPS to Garmin's login
service. The library receives an access/refresh token pair and, when a token
store is supplied, caches it locally for later sessions. Subsequent API methods
send that token to Garmin and can read or change the same account data that the
selected method targets.
The library does not automatically download an entire account. The demo writes
responses, activity downloads, and health reports only when you select those
actions. Demo exports are stored under your_data/ with owner-only directory
and file permissions. Run the project in a dedicated virtual environment, read
the method you plan to call, and start with read-only methods. Upload, edit,
delete, schedule, hydration, and weigh-in methods can change Garmin account data.
The default suite is credential-free and excludes live-account integration tests:
pdm run test # Run all tests
pdm run testcov # Run tests with coverage report
To explicitly run live integration tests, use a test account if possible. This can create local VCR recordings and includes methods that may mutate the account:
export GARMIN_EMAIL="you@example.com"
read -s GARMIN_PASSWORD && export GARMIN_PASSWORD
pdm run pytest -m integration --vcr-record=once
unset GARMIN_PASSWORD
VCR recordings are ignored by Git because Garmin responses contain sensitive health, activity, location, and account data. Do not commit them.
For package maintainers:
Setup PyPI credentials:
pip install twine
# Edit with your preferred editor, or create via here-doc:
# cat > ~/.pypirc <<'EOF'
# [pypi]
# username = __token__
# password = <PyPI_API_TOKEN>
# EOF
[pypi]
username = __token__
password = <PyPI_API_TOKEN>
Recommended: use environment variables and restrict file perms
chmod 600 ~/.pypirc
export TWINE_USERNAME="__token__"
export TWINE_PASSWORD="<PyPI_API_TOKEN>"
Publish new version:
pdm run publish # Build and publish to PyPI
Alternative publishing steps:
pdm run build # Build package only
pdm publish # Publish pre-built package
We welcome contributions! Here's how you can help:
Before contributing:
pdm run format && pdm run lintpdm run testExplore the API interactively with our reference notebook.
import os
from getpass import getpass
from datetime import date
from garminconnect import Garmin
# First run: logs in and saves tokens to ~/.garminconnect
# Subsequent runs: loads saved tokens and auto-refreshes
client = Garmin(
os.getenv("GARMIN_EMAIL"),
getpass("Garmin password: "),
prompt_mfa=lambda: input("MFA code: "),
)
client.login("~/.garminconnect")
# Get today's stats
today = date.today().isoformat()
stats = client.get_stats(today)
# Get heart rate data
hr_data = client.get_heart_rates(today)
print(f"Resting HR: {hr_data.get('restingHeartRate', 'n/a')}")
The library includes optional typed workout models for creating type-safe workout definitions:
pip install garminconnect[workout]
from garminconnect.workout import (
RunningWorkout,
WorkoutSegment,
create_warmup_step,
create_interval_step,
create_distance_interval_step,
create_cooldown_step,
create_repeat_group,
)
# Create a structured running workout
workout = RunningWorkout(
workoutName="Easy Run",
estimatedDurationInSecs=1800,
workoutSegments=[
WorkoutSegment(
segmentOrder=1,
sportType={"sportTypeId": 1, "sportTypeKey": "running"},
workoutSteps=[create_warmup_step(300.0)],
)
],
)
# Upload and optionally schedule it
result = client.upload_running_workout(workout)
client.schedule_workout(result["workoutId"], "2026-03-20")
# Edit it in place - keeps its id, so any schedules pointing at it stay valid
workout_data = client.get_workout_by_id(result["workoutId"])
workout_data["workoutName"] = "Easy Run (revised)"
client.update_workout(result["workoutId"], workout_data)
# Delete a workout or remove it from the calendar
client.delete_workout(workout_id)
client.unschedule_workout(scheduled_workout_id)
# Push a workout to a device - defaults to the last workout / last used device
client.push_workout_to_device(result["workoutId"], device_id)
Available workout classes: RunningWorkout, CyclingWorkout, SwimmingWorkout, WalkingWorkout, HikingWorkout, StrengthWorkout, MultiSportWorkout, FitnessEquipmentWorkout
Strength workouts are rep-based. Build each exercise with create_strength_set(category, step_order, sets, reps, rest_seconds, exercise_name="", weight_kg=None) and identify exercises with a category / exercise pair from the bundled catalog in garminconnect.exercises (1,527 exercises across 47 categories, with resolve(name) and find(term) helpers):
from garminconnect import exercises
from garminconnect.workout import StrengthWorkout, WorkoutSegment, create_strength_set
lat = exercises.resolve(
"Lat Pull-down"
) # {'category': 'PULL_UP', 'exercise': 'LAT_PULLDOWN'}
workout = StrengthWorkout(
workoutName="Upper Body",
estimatedDurationInSecs=0,
workoutSegments=[
WorkoutSegment(
segmentOrder=1,
sportType={"sportTypeId": 5, "sportTypeKey": "strength_training"},
workoutSteps=[
create_strength_set(
"BENCH_PRESS", step_order=1, sets=4, reps=10, rest_seconds=120
),
create_strength_set(
lat["category"],
step_order=4,
sets=3,
reps=12,
rest_seconds=90,
exercise_name=lat["exercise"],
),
],
)
],
)
client.upload_strength_workout(workout)
Helper functions: create_warmup_step, create_interval_step, create_distance_interval_step, create_recovery_step, create_cooldown_step, create_repeat_group, create_strength_exercise_step, create_strength_rest_step, create_strength_set
Use create_distance_interval_step(600.0, step_order=1) for interval steps that should end after a distance in meters instead of after a duration.
The library supports reading Garmin Golf scorecards, shot-level data, club statistics, and golf user statistics.
# List recent golf scorecards
summary = client.get_golf_summary(limit=10)
scorecard_id = summary["scorecardSummaries"][0]["id"]
# Get the full scorecard details
scorecard = client.get_golf_scorecard(scorecard_id)
# Get shot-by-shot data
# - Omit hole_numbers to receive every hole (recommended for holes 10-18).
# - Single-digit holes work as a comma- or dash-separated list, e.g. "1,2,3".
# Commas are normalized to dashes because Garmin only accepts '-' as the
# separator.
# - Double-digit holes (10-18) cannot be fetched with a filter at all; the
# API either drops them or returns an empty response. If you request holes
# 10-18, the library falls back to returning all 18 holes.
shots = client.get_golf_shot_data(scorecard_id)
shots = client.get_golf_shot_data(scorecard_id, hole_numbers="1,2,3")
# Club and player statistics
club_stats = client.get_golf_club_stats()
user_stats = client.get_golf_user_stats()
tests/ directorySpecial thanks to all contributors who have helped improve this project:
This project thrives thanks to community involvement and feedback.
If you find this library useful for your projects, please consider supporting its continued development and maintenance:
Why Support?
Every contribution, no matter the size, makes a difference and is greatly appreciated! π
(top 30 of 77)
Python
100.0%