A Django app for the Garmin Health API (Garmin Connect Developer Program), backed by django-healthdatamodel.
Project description
django-garmin
A reusable Django app for the Garmin Health API (Garmin Connect Developer Program). Handles Garmin's OAuth 2.0 + PKCE flow, fetches wellness summaries from apis.garmin.com, receives push/ping webhook notifications, and persists everything through django-healthdatamodel so the same storage and query layer serves Apple Health, Fitbit, Google Health, and Garmin side-by-side.
Garmin's Health API is partner-gated: you need an approved Garmin Connect Developer Program app (an evaluation-tier app works) to obtain a consumer key/secret. OAuth 1.0a is retired at the end of 2026; this package speaks only the current OAuth 2.0 + PKCE flow.
Install
pip install django-garmin
Add both this app and django-healthdatamodel to INSTALLED_APPS, then run migrations:
INSTALLED_APPS = [
...
"healthdatamodel",
"garmin",
]
python manage.py migrate
The model uses settings.AUTH_USER_MODEL so it works with any custom user model.
Configuration
GARMIN_CLIENT_ID = "..." # consumer key from the Garmin developer portal
GARMIN_CLIENT_SECRET = "..." # consumer secret
GARMIN_REDIRECT_URI = "https://your-app.example.com/garmin/callback/"
Register the redirect URI in the Garmin developer portal — the token exchange fails on any mismatch. Garmin has no per-request scope parameter: the API sections your app can reach are fixed at app creation, and each user picks permissions (e.g. HEALTH_EXPORT, ACTIVITY_EXPORT) on the consent screen. The permissions a user actually granted are stored on their GarminConnection.permissions.
URLs
urlpatterns = [
...
path("garmin/", include("garmin.urls")),
]
This mounts:
garmin/connect/— start the OAuth 2.0 + PKCE flow (login required)garmin/callback/— the redirect URI targetgarmin/disconnect/— POST; callsDELETE user/registrationat Garmin (required by their terms when you offer your own disconnect) and marks the connection revokedgarmin/notifications/— webhook receiver for push/ping notifications
Mobile clients that run the OAuth dance themselves can POST the resulting token dict to a project-local endpoint that calls garmin.oauth.ingest_tokens.
Getting data
Garmin delivers data two ways; this package supports both.
Webhooks (push/ping) — Garmin's recommended architecture. Configure your summary-type endpoints in the developer portal to point at garmin/notifications/, then connect a signal handler:
from django.dispatch import receiver
from garmin.signals import notification_received
from garmin.webhooks import process_notification
@receiver(notification_received)
def on_notification(sender, payload, **kwargs):
process_notification(payload) # or hand off to celery / a queue
process_notification routes entries to users by Garmin's stable userId, fetches callbackURLs for ping-style notifications, and ingests everything into healthdatamodel. Garmin expects a fast 200 and disables endpoints that keep failing — hand off to a queue if your processing is heavy.
Pull. garmin.ingest.sync_user(connection, start=..., end=...) fetches and ingests the configured summary types. Note Garmin's pull semantics: the window filters by upload time (when the user's device synced), capped at 24 hours per request (the client chunks longer windows automatically). For deep history, request a backfill (GarminClient.request_backfill or manage.py sync_garmin --backfill); Garmin re-sends the data asynchronously through your webhook endpoints.
python manage.py sync_garmin # last 24h of uploads, all active connections
python manage.py sync_garmin --user alice --days 7
python manage.py sync_garmin --summary-type dailies --summary-type sleeps
python manage.py sync_garmin --user alice --days 90 --backfill
What gets stored
This app does not define Record / Workout tables — those live in django-healthdatamodel. The garmin.ingest module maps Garmin summaries to healthdatamodel inputs and persists them with source="garmin":
| Garmin summary | healthdatamodel records |
|---|---|
dailies |
steps, active + BMR calories, distance, floors, resting HR, intensity minutes, intraday heart-rate samples |
sleeps |
one sleep-stage record per sleepLevelsMap interval (deep/light/rem/awake) |
bodyComps |
weight, body-fat %, BMI |
pulseox |
SpO2 samples |
activities |
one Workout per activity |
epochs |
steps/calories/distance at 15-minute granularity — opt-in, double-counts against dailies |
Garmin reports BMR directly (bmrKilocalories), so unlike the Google Health integration there is no BMR-estimation step.
The only model defined here is GarminConnection: per-user OAuth tokens (Garmin rotates the refresh token on every refresh — both are rewritten together), granted permissions, connection status, and last sync timestamp.
Try it on your own data
The repo includes a runnable demo Django project at demo/.
1. Get Garmin developer program access (one-time)
Apply at the Garmin Connect Developer Program — approval unlocks an evaluation environment. Create an app to get a consumer key/secret, and register the redirect URI http://localhost:8000/garmin/callback/ (exact match, trailing slash included).
2. Run the demo
uv sync
uv run python manage.py migrate
uv run python manage.py createsuperuser
export GARMIN_CLIENT_ID=...
export GARMIN_CLIENT_SECRET=...
uv run python manage.py runserver
Open http://localhost:8000/, sign in with the superuser you just created, then:
- Click Connect Garmin → consent on Garmin Connect → land back on the homepage with a
GarminConnectionsaved for your user. - Sync your watch in the Garmin Connect mobile app (pull windows filter by upload time), then click Sync now.
- Browse the resulting rows at
/admin/healthdatamodel/record/(and.../workout/).
Or drive sync from the terminal:
uv run python manage.py sync_garmin --user <your-username>
Documentation
Garmin's Health API docs are partner-gated, so summaries live under docs/garmin/:
oauth2-pkce.md— the OAuth 2.0 + PKCE flow (endpoints, parameters, token lifetimes)health-api.md— pull endpoints, summary payload shapes, push/ping webhooks, backfill
Development
uv sync --group dev
uv run pytest tests/ -v
uv run pre-commit run --all-files
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file django_garmin-0.1.0.tar.gz.
File metadata
- Download URL: django_garmin-0.1.0.tar.gz
- Upload date:
- Size: 62.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c85f0b3c3186302ff4d3bb58c8f72bc5f264cdfefff731653adb7ccfc7c5e12c
|
|
| MD5 |
ad4f41a06553effe105fd0f9fce2ff19
|
|
| BLAKE2b-256 |
2d8691d78c3aee3f43c6e8408839e955142b39517a2d7e99d7993be6149bf4cb
|
Provenance
The following attestation bundles were made for django_garmin-0.1.0.tar.gz:
Publisher:
ci.yml on django-health/django-garmin
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
django_garmin-0.1.0.tar.gz -
Subject digest:
c85f0b3c3186302ff4d3bb58c8f72bc5f264cdfefff731653adb7ccfc7c5e12c - Sigstore transparency entry: 2102141765
- Sigstore integration time:
-
Permalink:
django-health/django-garmin@fd440bf30208d15b2bdaa3d48af36eb5a56b1ee5 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/django-health
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
ci.yml@fd440bf30208d15b2bdaa3d48af36eb5a56b1ee5 -
Trigger Event:
push
-
Statement type:
File details
Details for the file django_garmin-0.1.0-py3-none-any.whl.
File metadata
- Download URL: django_garmin-0.1.0-py3-none-any.whl
- Upload date:
- Size: 27.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b5dc67d3640a7fad504d202f55011b5657372bb43caaeb1619ae7124ea8166fb
|
|
| MD5 |
7e0b0f80e888c809506569bf0b43de15
|
|
| BLAKE2b-256 |
ab03c779413ec2f2fd06de15cfc98c7e8f8c7edf2f554d48cd1aea141c691f12
|
Provenance
The following attestation bundles were made for django_garmin-0.1.0-py3-none-any.whl:
Publisher:
ci.yml on django-health/django-garmin
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
django_garmin-0.1.0-py3-none-any.whl -
Subject digest:
b5dc67d3640a7fad504d202f55011b5657372bb43caaeb1619ae7124ea8166fb - Sigstore transparency entry: 2102141848
- Sigstore integration time:
-
Permalink:
django-health/django-garmin@fd440bf30208d15b2bdaa3d48af36eb5a56b1ee5 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/django-health
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
ci.yml@fd440bf30208d15b2bdaa3d48af36eb5a56b1ee5 -
Trigger Event:
push
-
Statement type: