Skip to main content

Withings2Garmin

This is a fork. This repository builds on sodelalbert/Withings2Garmin, and borrows specific fixes from other forks/PRs in that project's family tree rather than reinventing them:

  • The migration to the garminconnect library (replacing a hand-rolled garth session client) follows the approach proposed by andrewleech in upstream PR #14.
  • The BMI field written to the FIT weight message follows the pattern added by eitanbehar in their fork (eitanbehar/Withings2Garmin, branch garmin-bmi-2026-working).

Most of the code on top of that baseline (packaging, the garminconnect migration itself, path resolution, tests) was assisted by an LLM (Claude Code).

Syncs Withings body measurements (weight, body composition, blood pressure) to Garmin Connect as FIT files.

Features

  • Withings OAuth 2.0 flow and Garmin Connect auth (including MFA), with tokens cached locally so you don't re-authenticate every run
  • Weight, body composition, and blood pressure sync, with BMI computed from Withings weight/height
  • Output to JSON and/or FIT file, in addition to (or instead of) uploading to Garmin Connect
  • Duplicate-safe: tracks what it's already uploaded and checks what Garmin already has before syncing (see Sync Safety), retries transient network failures, and won't corrupt its own state on a crash

Installation

This project requires Python 3.12+.

Released to PyPI as withings2garmin-hugoh (distinct from the upstream project's own package name, since this is a fork) on every merge to master — see Releases.

Run without installing (uvx)

With uv installed, run the tool directly from PyPI without a local checkout:

uvx --from withings2garmin-hugoh withings2garmin --garmin

Install for local development

curl -LsSf https://astral.sh/uv/install.sh | sh  # install uv

git clone https://github.com/hugoh/Withings2Garmin.git
cd Withings2Garmin
uv sync

Configuration

Environment Setup

Run withings2garmin --edit-config for an interactive prompt that saves your Garmin (and optionally Withings) credentials to the right config file for you — no need to find or hand-edit it, which is especially handy for the uvx no-checkout flow where there's no repo to find sample/.env.example in. Passwords/secrets are entered without being echoed to the terminal.

Alternatively, copy the example environment file and configure it by hand:

cp sample/.env.example .env

Edit .env with your credentials:

# Garmin Connect Configuration (required)
GARMIN_USERNAME=your_garmin_username
GARMIN_PASSWORD=your_garmin_password

# Withings API Configuration (optional - see below)
WITHINGS_CLIENT_ID=your_withings_client_id
WITHINGS_CLIENT_SECRET=your_withings_client_secret
WITHINGS_CALLBACK_URL=https://hugoh.github.io/Withings2Garmin/

Withings API Setup (optional)

WITHINGS_CLIENT_ID/WITHINGS_CLIENT_SECRET/WITHINGS_CALLBACK_URL are optional: if unset, this tool falls back to a shared Withings developer- app registration baked into withings_client.py (DEFAULT_CLIENT_ID/ DEFAULT_CLIENT_SECRET/DEFAULT_CALLBACK_URL, the latter currently https://hugoh.github.io/Withings2Garmin/, a simplified copy of the OAuth callback page used upstream by jaroslawhartman/withings-sync — most users don't need to register their own app. That shared app is rate-limited across everyone using it, so if you hit limits or want your own quota, register your own:

  1. Create a Withings developer account at Withings Developer Portal
  2. Create a new application
  3. Set the callback URL to: https://hugoh.github.io/Withings2Garmin/
  4. Set WITHINGS_CLIENT_ID/WITHINGS_CLIENT_SECRET in .env to override the default

Where files are stored

withings2garmin is a standalone CLI (installable via uvx), so it doesn't rely on a "repo root" for storing your credentials, tokens, or logs. It resolves each in order:

What 1. explicit override 2. cwd (local dev) 3. default
Config file (.env) $WITHINGS2GARMIN_CONFIG_FILE ./.env <user config dir>/config.env
Withings tokens $WITHINGS_TOKENS_FILE ./.withings_tokens.json <user data dir>/withings_tokens.json
Garmin session $GARMIN_SESSION_DIR ./.garmin_session <user data dir>/garmin_session
Logs $WITHINGS2GARMIN_LOG_DIR (always) <user log dir>

<user config/data/log dir> follow OS conventions via platformdirs — e.g. on Linux, ~/.config/withings2garmin, ~/.local/share/withings2garmin, and ~/.local/state/withings2garmin/log respectively; macOS and Windows use their native equivalents. You can also override the whole config/data/log directory at once with WITHINGS2GARMIN_CONFIG_DIR, WITHINGS2GARMIN_DATA_DIR, and WITHINGS2GARMIN_LOG_DIR.

If you keep running withings2garmin from the same directory (e.g. a repo checkout via uv run), files there are always found first — nothing changes for that workflow.

Run withings2garmin --show-paths to print all of the resolved paths above for your current environment.

Usage

Run withings2garmin --help for the full flag reference. A couple of common invocations:

# Sync to Garmin Connect (default date range: since last sync)
uv run withings2garmin --garmin

# Sync a specific date range and also save JSON/FIT files locally
uv run withings2garmin --garmin -f 2024-01-01 -t 2024-01-31 \
    --output-json backup.json --output-fit backup.fit

# See what would be uploaded without actually uploading or changing any state
uv run withings2garmin --garmin --dry-run --verbose

Authentication Workflow

First-Time Withings Authorization

When running for the first time, you'll see:

============================================================
WITHINGS AUTHORIZATION REQUIRED
============================================================
Open this URL in your browser and copy the authorization code:

https://account.withings.com/oauth2_user/authorize2?response_type=code&client_id=...

You have 30 seconds to complete this process!
============================================================
Enter authorization code: [paste code here]

Open the URL, authorize the application, and paste the resulting code back into the terminal. Tokens are then saved for future runs.

Garmin Multi-Factor Authentication

If MFA is enabled on your Garmin account, you'll be prompted for MFA code: on first login; the session is then saved locally for future runs.

Sync Safety

By default (no -f), each run only fetches data since the last successful sync, tracked in the Withings tokens file — a fresh install with no prior sync defaults to the last 24 hours, not full history (pass -f explicitly to backfill further back).

Within whatever range gets fetched, two independent checks keep a Garmin upload from creating duplicates:

  1. Local tracking — this tool records which Withings measurements it's already uploaded, so re-running over an overlapping range (e.g. a manual -f/-t, or a retry) skips anything it already pushed.
  2. Garmin-side check — for whatever's left, it queries Garmin Connect's actual existing weight/blood-pressure entries for that date range and skips anything already there, regardless of source (another sync tool, a manual entry, a different machine). Best-effort: if this check fails, it logs a warning and falls back to uploading (layer 1 is still the primary guarantee).

Two flags for working with this:

  • --dry-run — report what would be uploaded/skipped without uploading or changing any local state. Useful for a sanity check before trusting an automated (e.g. cron) run.
  • --force — bypass both checks for one run and upload everything fetched, e.g. if you know Garmin lost some data and want to intentionally re-push it. Still records what it uploaded, so a normal run afterward won't re-trigger on the same data.

Beyond dedup: transient Withings API failures (connection errors, timeouts) are retried automatically with backoff; the tokens file is written atomically (a crash mid-write can't corrupt it); and a file lock prevents two concurrent runs (e.g. an overlapping cron job) from racing on the same state.

Logging

Each run writes a timestamped log file under the log directory (see Where files are stored). --verbose raises the log level to DEBUG (including the resolved config/token/session paths); default is INFO.

Troubleshooting

Tokens/session files may be in your working directory or in the default user data directory — run withings2garmin --show-paths if unsure (see Where files are stored). --verbose also logs the resolved paths on every run.

Withings token expiration / Garmin session issues:

Run uv run withings2garmin --show-paths to find the Withings tokens and Garmin session dir paths, then:

# Remove invalid Withings tokens and re-authenticate
rm <withings-tokens-path>

# Or clear the Garmin session and re-authenticate
rm -rf <garmin-session-dir>

uv run withings2garmin --garmin

No measurements found: verify the date range (-f/-t) covers a period where your Withings device actually synced data.

Upgrading (breaking change: config/token/session/log locations)

.env, .withings_tokens.json, .garmin_session/, and logs/ used to be strictly relative to whatever directory you ran the tool from. They now default to OS-appropriate user config/data/log directories instead (see Where files are stored):

  • You keep running the tool from the same directory as before (e.g. uv run withings2garmin from a repo checkout): no change — that directory is still checked first for .env/tokens/session.
  • You run it from a new directory, a fresh install, CI, or a container: .env/tokens/session now resolve to the new default locations instead of erroring or silently treating you as unauthenticated with no explanation. Logs unconditionally move to the new default log directory (no cwd fallback, since there's no continuity to preserve for logs).
  • To pin the old cwd-relative behavior explicitly, set WITHINGS2GARMIN_CONFIG_FILE=./.env, WITHINGS_TOKENS_FILE=./.withings_tokens.json, GARMIN_SESSION_DIR=./.garmin_session, and WITHINGS2GARMIN_LOG_DIR=./logs.

Development

Linting/formatting/hygiene checks are defined once in hk.pkl and run via hk (tool versions pinned in mise.toml — run mise install first):

hk check   # verify (lint, format-check, GH Actions security/pinning, etc.)
hk fix     # auto-fix what can be fixed

# Run tests
uv run pytest

If your global git hooks already invoke hk (per-machine setup, not project-specific), hk check also runs automatically on commit/push. If not, run hk install once in this repo to wire that up locally.

Dependencies are managed in pyproject.toml; run uv lock --upgrade && uv sync to update them.

Releases

Commit messages must follow Conventional Commits (enforced by an hk commit-msg hook — feat: ..., fix: ..., etc.). Pushing to master automatically bumps the version, publishes a GitHub Release, and pushes the built package to PyPI when a commit warrants it: feat: → minor, fix: → patch, BREAKING CHANGE:/! → major. The package version itself is derived from the latest git tag (via hatch-vcs) — nothing to bump by hand. PyPI publishing uses Trusted Publishing (OIDC), so no API token is stored in the repo.

License

MIT — see pyproject.toml.

Metadata

Release files for withings2garmin-hugoh 1.5.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 withings2garmin-hugoh 1.5.0
File Size Uploaded
withings2garmin_hugoh-1.5.0.tar.gz 64.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for withings2garmin-hugoh 1.5.0
File Interpreter ABI Platform
withings2garmin_hugoh-1.5.0-py3-none-any.whl Python 3 none any Details

Total release size: 87.2 kB

Release files / withings2garmin_hugoh-1.5.0.tar.gz

Download URL withings2garmin_hugoh-1.5.0.tar.gz
Size 64.6 kB
Tags Source
SHA-256 checksum
How to use checksums
9653d52726fc6d7da3c408278e591cc254bc399a4e77e30c141474b0a4845b84
BLAKE2b-256 checksum
How to use checksums
b8268f7e79ab17ba533ba4d90515407543e14c0c3b95a43c77579c037e489208
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 5, 2026.

Transparency log

Release files / withings2garmin_hugoh-1.5.0-py3-none-any.whl

Download URL withings2garmin_hugoh-1.5.0-py3-none-any.whl
Size 22.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
62a01019b8e50112fc4197548de6e100d8311fef8a5de45614bd3d1e97a66b22
BLAKE2b-256 checksum
How to use checksums
d8ac1708441b8abb56574ad07629546448323d74cd77cf5eb786f7d7103ff5c0
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 5, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

1.5.0 This release

2 release files

1.4.1

2 release files

1.4.0

2 release files

1.3.1

2 release files

1.3.0

2 release files

1.2.0

2 release files

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