django-google-health
A reusable Django app for the Google Health API — the successor to the Fitbit Web API. Handles the Google OAuth 2.0 flow, fetches user health data from health.googleapis.com, and persists it through django-healthdatamodel so the same storage and query layer serves Apple Health, Fitbit, and Google Health side-by-side.
Google recommends launching new integrations after the end of May 2026 to align with legacy Fitbit account deprecation. See
docs/google-health/get-started.md.
Status
Early scaffolding. The package, demo project, OAuth model, and CI are in place. OAuth views, the HTTP client, ingest mapping, and webhook handling are stubbed and will land in follow-up slices.
Install
pip install django-google-health
Add both this app and django-healthdatamodel to INSTALLED_APPS, then run migrations:
INSTALLED_APPS = [
...
"healthdatamodel",
"googlehealth",
]
python manage.py migrate
The model uses settings.AUTH_USER_MODEL so it works with any custom user model.
Configuration
GOOGLE_HEALTH_CLIENT_ID = "..." # from Google Cloud Console
GOOGLE_HEALTH_CLIENT_SECRET = "..."
GOOGLE_HEALTH_REDIRECT_URI = "https://your-app.example.com/google-health/callback"
Set up the OAuth client in Google Cloud Console and enable the Google Health API. See docs/google-health/codelabs-make-your-first-api-call.md for a step-by-step walkthrough.
Mobile (backend-owned) OAuth flow
The session views (connect/callback) assume a logged-in browser. Mobile
apps get a backend-owned flow instead — tokens are minted by your
confidential web client (so they stay refreshable server-side), and no Django
session is needed:
- Your authenticated API endpoint (DRF view, FastAPI route, …) calls
googlehealth.oauth.start_mobile_flow(customer, deeplink="yourapp://google-health")and returns the consent URL to the app. - The app opens the URL in a system browser (ASWebAuthenticationSession / Chrome Custom Tab — don't follow it as a redirect).
- Google redirects to the public
googlehealth.views.mobile_callback(google-health/mobile/callback/, URL namegooglehealth:mobile_callback) — the customer is resolved from a single-use, TTL-boundedGoogleHealthOAuthStaterow, not a session. PointGOOGLE_HEALTH_REDIRECT_URI(and the Google Cloud client's authorized redirect URI) at wherever you serve it. - The callback 302s the browser to the app's deep link:
<deeplink>?status=success|denied|error[&reason=...]. - On success the
googlehealth.signals.mobile_connectedsignal fires withcustomerandconnection— hook it to activate the data source, kick off a first sync, etc.
Related settings (all optional):
GOOGLE_HEALTH_APP_DEEPLINK = "yourapp://google-health" # default deep link; must be a non-http(s) app scheme
GOOGLE_HEALTH_MOBILE_STATE_TTL_MINUTES = 10 # state row time-to-live
GOOGLE_HEALTH_DEFAULT_SCOPES = [...] # shared with the session flow
The API endpoint and the callback can run in separate deployments (e.g. a Lambda API and a Kubernetes web tier) — they only need to share the database.
Scopes
Google Health scopes are namespaced under https://www.googleapis.com/auth/googlehealth.*. The complete list lives in googlehealth.constants and is documented in docs/google-health/scopes.md. Examples:
googlehealth.activity_and_fitness.readonly— steps, distance, exercise, floors, altitudegooglehealth.health_metrics_and_measurements.readonly— heart rate, weight, body fat, SpO2googlehealth.sleep.readonly— sleep stages and sessionsgooglehealth.location.readonly— exercise GPSgooglehealth.profile.readonly— DOB and gender, used bycompute_basal_caloriesfor a real Mifflin-St Jeor BMR instead of a median fallback
DEFAULT_SCOPES (overridable via GOOGLE_HEALTH_DEFAULT_SCOPES) requests
activity_and_fitness, health_metrics_and_measurements, profile, and sleep,
all readonly. Existing connections must disconnect and reconnect to pick
up a newly added scope — a stored refresh token doesn't gain scopes
retroactively, so get_profile will keep 403ing for anyone who connected
before this scope was added.
Storage
This app does not define Record / Workout tables — those live in django-healthdatamodel. The googlehealth.ingest module maps Google Health API responses to healthdatamodel.schemas.RecordInput and WorkoutInput, then calls healthdatamodel.ingest.ingest_records to persist them. Read the data back with healthdatamodel.query.* (see that project's docs).
The models defined here are GoogleHealthConnection (per-user OAuth tokens, granted scopes, connection status, and last sync timestamp) and GoogleHealthOAuthState (short-lived single-use state rows for the mobile flow).
Documentation
The Google Health API documentation is vendored as Markdown under docs/google-health/ so it's grep-able offline:
get-started.md— overview, benefits, getting started pathsmigration.md— Fitbit Web API → Google Health API migration guidedata-types.md— every data type with operations and scopesscopes.md— OAuth scopeswebhooks.md— subscriber registration, endpoint verification, notification payloadscodelabs-make-your-first-api-call.md— end-to-end OAuth + first API callreference-rest.md— REST resource indexmigration-parity-tool.md— parity tool referencesupport.md— issue tracker and forum links
Try it on your own data
The repo includes a runnable demo Django project at demo/ that takes you
through the full OAuth flow and syncs your Google Health data into
healthdatamodel. The same OAuth setup also unlocks the
@pytest.mark.live integration tests.
1. Set up a Google Cloud OAuth client (one-time)
Walkthrough in docs/google-health/codelabs-make-your-first-api-call.md. The
specifics that matter for the demo:
- Application type: Web application.
- Authorized redirect URI:
http://localhost:8000/google-health/callback/(exact match — Google compares byte-for-byte, including the trailing slash). - Under Audience, set publishing status to Testing and add your Google account as a Test user.
- Under Data Access, add the scopes you want. A good starter set:
googlehealth.activity_and_fitness.readonly,googlehealth.health_metrics_and_measurements.readonly,googlehealth.profile.readonly,googlehealth.sleep.readonly.
One real-world prerequisite: the Google Health API serves data from a
Fitbit profile. Install the Fitbit mobile app, sign in with the same Google
account, and (optionally) log a manual activity so there's something to fetch.
Without this, even authenticated calls return 400 The account is not linked to Google Health. (See issue
#2 for the
follow-up around backfilling identity after the link is created.)
2. Run the demo
uv sync
uv run python manage.py migrate
uv run python manage.py createsuperuser
export GOOGLE_HEALTH_CLIENT_ID=...
export GOOGLE_HEALTH_CLIENT_SECRET=...
# oauthlib refuses non-HTTPS redirect URIs by default. Fine for local dev:
export OAUTHLIB_INSECURE_TRANSPORT=1
uv run python manage.py runserver
Open http://localhost:8000/, sign in with the superuser you just created, then:
- Click Connect Google Health → consent on Google → land back on the
homepage with a
GoogleHealthConnectionsaved for your user. - Pick a window + resolution and click Sync now to fetch and persist records.
- Browse the resulting rows at
/admin/healthdatamodel/record/(and.../workout/).
If you'd rather drive sync from the terminal:
uv run python manage.py sync_google_health --user <your-username> --days 7
3. (Optional) Enable the live integration tests
A handful of tests are marked @pytest.mark.live and hit the real
health.googleapis.com. They self-skip unless three env vars are set.
After step 2 has run at least once, the demo's OAuth round-trip has already
deposited a long-lived refresh_token in db.sqlite3 — reuse it:
export GOOGLE_HEALTH_TEST_CLIENT_ID=$GOOGLE_HEALTH_CLIENT_ID
export GOOGLE_HEALTH_TEST_CLIENT_SECRET=$GOOGLE_HEALTH_CLIENT_SECRET
export GOOGLE_HEALTH_TEST_REFRESH_TOKEN=$(sqlite3 db.sqlite3 \
"SELECT refresh_token FROM googlehealth_googlehealthconnection LIMIT 1;")
uv run pytest tests/ -v -m live
The default pytest run still skips them.
A separate scheduled workflow (.github/workflows/live.yml) runs these
tests nightly against the real API, gated on three repo secrets of the
same names: GOOGLE_HEALTH_TEST_CLIENT_ID, GOOGLE_HEALTH_TEST_CLIENT_SECRET,
and GOOGLE_HEALTH_TEST_REFRESH_TOKEN. Until those secrets are configured
on the repo (and on forks, where they're never available), the job checks
for them and exits neutrally instead of failing.
Token-expiry caveat (as of 2026-07-22): while the Google Cloud OAuth
consent screen for this app is in "testing" mode, Google expires refresh
tokens after 7 days regardless of use — the 6-month idle-expiry behavior
people usually expect only kicks in once the consent screen is verified
and published to production. Until then, GOOGLE_HEALTH_TEST_REFRESH_TOKEN
needs re-minting roughly weekly (repeat the sqlite3 command above after a
fresh OAuth round-trip). If the nightly job starts failing with
invalid_grant, re-mint the token before assuming the code regressed.
Development
uv sync --group dev
uv run pytest tests/ -v
uv run pre-commit run --all-files
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_google_health-0.5.0.tar.gz.
File metadata
- Download URL: django_google_health-0.5.0.tar.gz
- Upload date:
- Size: 134.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e91eb879e0d54c792afa8623c4b40a890fc1ab2294a3f9423d26c637733e8490
|
|
| MD5 |
046292e073107f56bd726ddc1a07258e
|
|
| BLAKE2b-256 |
9bf1b173c08786f539e8a94fc02cc28d1a8bf9a26c3c1eae2a8d1e460e9c1222
|
Provenance
The following attestation bundles were made for django_google_health-0.5.0.tar.gz:
Publisher:
ci.yml on django-health/django-google-health
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
django_google_health-0.5.0.tar.gz -
Subject digest:
e91eb879e0d54c792afa8623c4b40a890fc1ab2294a3f9423d26c637733e8490 - Sigstore transparency entry: 2304417104
- Sigstore integration time:
-
Permalink:
django-health/django-google-health@29a864240cd9a233a77d225f435a5f7e95523a40 -
Branch / Tag:
refs/tags/v0.5.0 - Owner: https://github.com/django-health
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
ci.yml@29a864240cd9a233a77d225f435a5f7e95523a40 -
Trigger Event:
push
-
Statement type:
File details
Details for the file django_google_health-0.5.0-py3-none-any.whl.
File metadata
- Download URL: django_google_health-0.5.0-py3-none-any.whl
- Upload date:
- Size: 43.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
718baaf30192c7043d52cf8d0d9576569e7639734d6c08b5db8f9bf5518bd63b
|
|
| MD5 |
71396e6d72660f6bd05e6dc18f9bc0a0
|
|
| BLAKE2b-256 |
962e2f3852a75d029d573b82010c1522216a8ee5d0d9993a5257da85c03e0dd2
|
Provenance
The following attestation bundles were made for django_google_health-0.5.0-py3-none-any.whl:
Publisher:
ci.yml on django-health/django-google-health
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
django_google_health-0.5.0-py3-none-any.whl -
Subject digest:
718baaf30192c7043d52cf8d0d9576569e7639734d6c08b5db8f9bf5518bd63b - Sigstore transparency entry: 2304417153
- Sigstore integration time:
-
Permalink:
django-health/django-google-health@29a864240cd9a233a77d225f435a5f7e95523a40 -
Branch / Tag:
refs/tags/v0.5.0 - Owner: https://github.com/django-health
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
ci.yml@29a864240cd9a233a77d225f435a5f7e95523a40 -
Trigger Event:
push
-
Statement type: