Repository navigation
Releases: the-momentum/open-wearables
Release list
0.10 - New dashboard & nutrition data
We encourage you to use our official Docker images:
- backend: https://hub.docker.com/r/themomentum/open-wearables-backend
- frontend: https://hub.docker.com/r/themomentum/open-wearables-frontend
Highlights
This release brings a rebuilt dashboard and support for nutrition data, with 42 new data types. Outgoing webhooks now tell you which sync each event came from, and calories and data sources are more accurate across providers. Plus a lot of smaller fixes and improvements, 123 PRs in total.
0.11 is coming very soon and focuses on further data model improvements, as we head towards Open Wearables 1.0. If there's something you think we should include or consider for 1.0, let us know on Discord or in Discussions. Feedback from teams that have been running Open Wearables for a while is especially welcome.
Caution
Breaking changes in 0.10.0. Most of them probably don't affect you at all, as they only touch specific providers or edge cases, but read the list carefully before upgrading.
active_calories_kcalis nownullinstead of0when a day has no active energy, for every provider.total_calories_kcalin/summaries/activityisnullwhen a day has neither total nor basal energy. (#1801)- Whoop cycle energy is stored as the new
total_energyseries instead ofactive_energy, because it includes BMR. Webhooks for Whoop cycles now sendseries.total_energy.createdinstead ofseries.active_energy.created. A startup migration relabels stored rows and drops 0 kJ no-wear cycles. (#1801) - Ultrahuman active minutes move from
active_timetoexercise_time, and outgoing webhooks sendseries.exercise_time.createdinstead ofseries.active_time.created.active_minutesin/summaries/activityfalls back to the step-based estimate for Ultrahuman users. A startup migration relabels stored rows. (#1765) - Ultrahuman HRV moves from
heart_rate_variability_sdnntoheart_rate_variability_rmssd(confirmed by Ultrahuman). A startup migration relabels stored rows. Users whose only HRV source is Ultrahuman now get a Resilience score. (#1686) - Workouts from the mobile SDK and Apple XML import that report no calories now have
nullcalories instead of0. A startup migration clears the stored zeros. (#1874) - SDK refresh tokens of deleted applications are removed. Their users get a
401on the next refresh and the SDK reports an auth error (Handle expired sessions). (#1841)-
Check for SDK users of deleted applications
SELECT rt.app_id, count(*) AS active_tokens, count(DISTINCT rt.user_id) AS users, max(coalesce(rt.last_used_at, rt.created_at)) AS last_activity FROM refresh_token rt LEFT JOIN application a ON a.app_id = rt.app_id WHERE rt.token_type = 'sdk' AND rt.revoked_at IS NULL AND a.id IS NULL AND split_part(rt.app_id, ':', 1) NOT IN ('admin', 'invite') GROUP BY rt.app_id ORDER BY last_activity DESC;
Apps that handle the SDK auth error by getting new tokens from their backend and passing them to
updateTokens()recover on their own, as long as that backend uses the credentials of an application that still exists.
-
- Outgoing webhook payloads are kept in Svix for 7 days instead of 90. Change it with
OUTGOING_WEBHOOK_PAYLOAD_RETENTION_DAYS(5-90). Payloads already stored keep their 90-day expiry. (#1719) - 24/7 sync results have a new, typed shape.
data_247is now an object withstatus, item counters and one entry per provider task, replacing the flat map of task names to row counts and thesuccess/savedflags. It changes in the sync endpoint response (details.data_247), insync.completed/sync.failedwebhooks (metadata.params.data_247) and in sync status events. A sync where every 24/7 task fails now returnssuccess: falseand emitssync.failed. Error fields carry a short code (http_401,IntegrityError) instead of the exception text, anderrorinparams.workoutsis renamed toerror_code. (#1501)
Note
The first startup after upgrading can take longer than usual: data migrations merge duplicated data sources and relabel the rows listed above. Expensive migrations are now recorded in a new data_migration table and run once per instance (Data migrations).
New dashboard
The dashboard is rewritten in SvelteKit by @KaliszS 🪄. The layout is cleaner, there are light and dark themes (or it follows your system setting), and it works on phones too, with a bottom navigation bar.
Heavy pages load faster, and Data Summary opens on the last 30 days by default. Tabs on the user profile have period presets (1W / 1M / 3M / 6M / 1Y) and a provider filter, and the Sleep tab can also show only night sleep, only naps, or only the highest-priority source. Each record shows which provider and device it came from.
The user profile also got a Meals tab, a Women's Health tab that's always visible (empty when there's no data), and data sources listed in priority order.
Note
The image name, port 3000 and VITE_API_URL are unchanged, so no configuration change is needed. Developers sign in again once after the upgrade. VITE_API_URL is now read at runtime, so one image works against any backend without a rebuild. Optionally set API_URL when the frontend can reach the API on an internal address, e.g. http://app:8000 on the same Docker network.
More: Frontend configuration
Nutrition data
Open Wearables now stores nutrition data, thanks to @McArthur955 🎉. There are 42 new series types: calories, carbs, protein, fat (total, saturated, mono- and polyunsaturated, trans), fiber, sugar, cholesterol, caffeine, and a full set of vitamins and minerals. Apps that log food as meals also get a new meal event type with a title, meal type and nutrients. Not every app does that, so nutrition samples logged outside a meal are stored as regular time series.
This is most useful with Apple Health. Food trackers like MyFitnessPal, Lose It! and Cronometer, and dozens of other apps, write what users eat into it, so you get their data without integrating each app separately. Apple Health covers 38 of the nutrition types, Health Connect all 42 and Samsung Health 17. The Google Health API nutrition log is supported too.
Note
For the mobile SDK, nutrition is supported on the backend side only for now. SDK versions that send meals from Apple Health, Samsung Health and Health Connect haven't been released yet, but they're coming soon - follow the iOS SDK and Android SDK repos. Google Health API nutrition works with this release.
GET /api/v1/users/{user_id}/events/meals?start_date=2026-10-05&end_date=2026-10-05
DELETE /api/v1/users/{user_id}/events/meals/{meal_id}
The seed generator can create meals too, with a nutrition_focused preset.
More: Nutrition data types
Outgoing webhooks know which sync they came from
Every data webhook now carries a sync block, so a consumer can tell a historical backfill from live sync:
source is pull, webhook, sdk, backfill, xml_import or linked_account, and run_id joins to GET /sync/history/{run_key}. Two new events, sync.data_type.completed and sync.data_type.failed, report the result per data type, with item counts and the written and covered time ranges. Existing payloads only gain the new key.
Calories: total, active and basal
Providers that report only a total with no active/basal split now write to a new total_energy series (today that's Whoop), with a series.total_energy.created webhook. Basal energy is now stored for Garmin, Polar, Oura and SensorBio, and total_calories_kcal in summaries comes from total_energy first, with active + basal as the fallback. Whoop cycle metrics are also anchored on the right local day, so users in negative UTC offsets no longer get two cycles summed into one day.
Stable data sources
One device or app no longer splits into several data sources. A source is now matched by device ID, then by app bundle ID and model, and an existing source gets upgraded when better data arrives. In practice: renaming an iPhone, upgrading the phone that relays third-party Apple Health data, or Samsung sending steps without IDs no longer creates a new source. A startup migration merges the duplicates already stored. New device types were added as well: chest straps, earbuds, tablets, ...
0.9.0 - Google Health pack & breaking changes!
We encourage you to use our official Docker images:
- backend: https://hub.docker.com/r/themomentum/open-wearables-backend
- frontend: https://hub.docker.com/r/themomentum/open-wearables-frontend
Highlights
This release splits the Google integration into two providers, renames the energy series to active_energy, and fixes a long list of API inconsistencies around date bounds, filters and pagination.
Caution
Breaking changes in 0.9.0. Read this list before upgrading.
- Google is now two providers.
googleis replaced bygoogle_health(cloud OAuth, Google Health API) andhealth_connect(Android SDK). Outgoing webhooks carry the new slugs insource.provider,connection.created/connection.revokedand sync events, so anything filtering onprovider == "google"stops matching. A data migration runs automatically on startup. energyis renamed toactive_energyin every response body, and the granular webhook eventseries.energy.createdbecomesseries.active_energy.created. Requests still accepttypes=energyuntil 1.0. Webhook subscribers need the migration script below.- Google Health API rollup is disabled. A provider with
data_granularityset to anything other thanrawnow fails those data types withUnsupportedGranularityErrorinstead of writing duplicated, inflated rows. - Existing Google Health API energy data is wrong and has to be purged with an optional script, then re-synced.
- Date-only
end_date/end_timenow includes the whole end day. If you compensated by passing the next day, you will get one extra day back. /events/sleepno longer returnssleep_stage_intervalsby default. Pass?include=stages. Unreported stages are nownullinstead of0./timeseriesreturnstotal_count: nullon cursor pages and on every aggregated (resolution != raw) read.resolutionon/timeseriesis no longer ignored. Requests that passed it and silently got raw samples now get aggregated buckets.
Google Health and Health Connect are now two providers
google covered two unrelated integrations under one slug: data pushed from Android through the mobile SDK, and the Google Health API cloud OAuth flow. They now have their own provider identities, their own connection rows and their own line on the coverage matrix.
| Old | New | What it is |
|---|---|---|
google |
google_health |
Google Health API, server side OAuth, REST pulls and notify-only webhooks |
google |
health_connect |
Health Connect data uploaded through the mobile SDK |
GET /api/v1/users/{user_id}/connections
{ "data": [ { "provider": "google_health", "status": "active" }, { "provider": "health_connect", "status": "active" } ] }Filters follow the same names: ?provider=google_health, ?provider=health_connect.
What keeps working
- Webhooks already registered with Google against
/api/v1/providers/google/webhooksstill deliver. Both/google/and/google_health/paths are served; new registrations use/google_health/. - Existing user connections with periodic sync keep running with no action.
- The mobile SDK still sends
googleas the provider name in the payload body for Health Connect.googleis kept as an alias forhealth_connect.
What breaks
Warning
Outgoing webhooks now send google_health or health_connect in provider. There is no alias here because it would be ambiguous. Affected payloads: connection.created, connection.revoked, source.provider on data events, and sync events. Update any consumer that filters on provider == "google".
Raw payload storage in S3 also splits into two provider paths instead of one google prefix. This only matters if you read raw payloads for debugging.
New OAuth path and GOOGLE_LEGACY_OAUTH_PATH
The OAuth redirect URI must match what is registered in Google Cloud, so the path is behind a flag:
GOOGLE_LEGACY_OAUTH_PATH=true # default, sends the old /api/v1/oauth/google/callback
GOOGLE_LEGACY_OAUTH_PATH=false # sends /api/v1/oauth/google_health/callback
Existing deployments need no change: the default keeps your registered client working. Both inbound paths are active, the flag only controls the redirect_uri Open Wearables sends to Google.
New integrations, and anyone who wants to be on the supported path before 1.0, should register {API_BASE_URL}/api/v1/oauth/google_health/callback in Google Cloud (Google accepts several redirect URIs, so both can coexist during the migration) and set GOOGLE_LEGACY_OAUTH_PATH=false.
Note
The legacy path and the flag are removed in 1.0.
Google Health API data corrections
Rollup is disabled
The windowed rollUp operation anchors its buckets to the requested range start, so every lookback or manual sync rewrote the same hour at a different timestamp:
{"timestamp": "2026-09-08T08:01:30Z", "value": 70.728}
{"timestamp": "2026-09-08T08:01:33Z", "value": 70.728}It is now off. With the default data_granularity = raw nothing changes. Set to hourly or daily, the affected data types are skipped, the run is marked partial, and you get this in the logs and in Sentry:
UnsupportedGranularityError
Google Health data_granularity 'hourly' needs the windowed rollUp operation, which is disabled (#1577). Its data types were skipped; set the provider's granularity to 'raw' to resume them. Sleep and derived daily totals are unaffected.
Caution
If google settings have data_granularity set to anything other than raw, change it back before upgrading. Sleep sessions and derived daily totals sync either way.
Active and basal energy
Total calories used to be written into the energy series, which means active energy, so values were inflated by BMR and multiplied by repeated syncs. Active energy now comes from the native active-energy-burned intervals (list / reconcile), tagged with an external_id, and basal energy is derived per civil day as total calories minus active energy from dailyRollUp, stored as a daily total.
Migration: purge the bad energy rows (optional, manual)
Run once after deploying and before any historical re-sync. It deletes untagged legacy rows in the live table, plus every archived Google Health API energy bucket (the archive carries no per-row marker, and total-calories was its only writer before this fix).
uv run python scripts/data_migrations/purge_google_total_calories_energy.py --dry-run
uv run python scripts/data_migrations/purge_google_total_calories_energy.py--batch (default 50000) commits per batch so no single transaction holds locks; --skip-archive if archival already ran over re-synced data. Not wired into startup, because it removes data only a re-sync can bring back.
New data types and other fixes
- New series ingested:
heart_rate_variability_rmssd(fromdaily-heart-rate-variability),vo2_max,oxygen_saturation(fromdaily-oxygen-saturation). Webhook subscriptions should include the twodaily-types. - Zero-valued samples are no longer stored, so HRV and SpO2 averages stop being dragged toward 0, and a day with no activity has no row instead of a
0row. - Daily series (resting heart rate, respiratory rate, daily HRV, daily SpO2) are written with
is_daily_total: null, in line with every other provider, so they now appear in bucketed/timeseries?resolution=...reads. Rows written before this release keepis_daily_total: trueand stay excluded. - Live pulls import overnight sleep sessions (matched on session end, not start) and the previous day's daily totals, so the first pull after upgrading may bring in one extra night and one extra day.
- Each data type commits on its own, so a failure in one no longer discards the others.
- A malformed or error-shaped page from Google fails that metric and marks the run
partial, instead of reading as an empty window and reportingsuccess.
Sync reliability and new settings
| Variable | Default | Purpose |
|---|---|---|
PROVIDER_REQUEST_TIMEOUT_SECONDS |
30 |
Per-request timeout for provider API calls, previously hard coded |
LINKED_SYNC_PULL_LEASE_SECONDS |
120 |
Lease for the linked-account pull lock, renewed while the sync runs |
Both optional, defaults keep current behaviour.
linked_sync:<provider>:<account>:pull:primary now expires around two minutes after its holder dies instead of up to four hours, so a manual redis-cli DEL after a worker restart is no longer needed. The Garmin backfill lock keeps its 4 hour TTL.
A linked-account pull that finds another profile syncing the same account now records a run with status: "skipped", items_processed: 0 and metadata.skipped: true in GET /users/{user_id}/sync/runs, instead of recording nothing and reporting success. last_synced_at is not advanced on a skip, so the primary's fan-out covers the full gap.
energy is now active_energy
energy sat next to basal_energy and looked like the total. It was active energy only, so it is renamed. The stored series id is unchanged (81), no data moves, and series_type_definition.code is rewritten by the existing startup seed.
Requests still accept the old name
GET /timeseries?...&types=energy -> 200, "type": "active_energy"
GET /timeseries?...&types=active_energy -> 200, "type": "active_energy"
GET /timeseries?...&types=nonsense -> 400
Responses always return the new name
Warning
Anything matching the literal "energy" in a response body breaks: type on /timeseries, the key in /summaries/data and in /summaries/data/timeline?group_by=series_type, and code in /meta/coverage. Request values keep working until 1.0.
GET /summa...0.8.0 - Withings & Historical Syncs records
Highlights
We encourage you to use our official Docker images:
- backend: https://hub.docker.com/r/themomentum/open-wearables-backend
- frontend: https://hub.docker.com/r/themomentum/open-wearables-frontend
Note
We have changed the way we tag images:
latest- this is how you can get the newest stable image0.8.0,0.8- this is how you can get the chosen stable releasenightly- unstable, but always up to date with main (we deploy this tag every night)
Caution
A warning for nightly users regarding instability (migration #9b079...): If you have already used the nightly tag and carried out an update between 2026-09-07T17:46:43Z and 2026-09-08T14:55:55Z, please check whether your database contains the columns entry_source, intensity and label in the workout_details table. If not, this means you downloaded a version containing a faulty migration, in which case we recommend adding these columns manually using SQL:
ALTER TABLE workout_details
ADD COLUMN IF NOT EXISTS entry_source VARCHAR(32),
ADD COLUMN IF NOT EXISTS intensity VARCHAR(10),
ADD COLUMN IF NOT EXISTS label VARCHAR(255);Withings integration
We now have integration with a new provider, Withings! This integration introduces new elements to our global architecture. We are making all data categories available straight away, along with the methods for integrating them. Thank you to our contributor for preparing the pull request!
Historical Sync metadata preserved
Every sync now leaves a trace. We introduced a sync_run table that tracks a complete synchronization run - provider, scope (historical or live), status, timing, item counts and errors - plus a per-data-type breakdown within another table sync_run_data_type, so you get one row per data type per run instead of an opaque blob. Runs are tracked across all ingestion paths: provider pulls & pushes, SDK uploads, SDK logs, Apple XML imports and Garmin backfills. A stale sweep closes runs that stopped reporting, checking Redis for liveness first so a slow backfill isn't cut off prematurely. Two new endpoints expose it: GET /users/{id}/sync/history and GET /sync/history/{run_key}. It will be also added to our frontend in its next iteration.
Live syncs (webhooks, scheduled pulls, incremental SDK batches) are tracked in Redis for status updates but not stored by default, since they would add hundreds of rows a day per active user - set PERSIST_LIVE_SYNC_RUNS=true if you want them in the table too.
SDK uploads queue a reference instead of the payload
Passing the full SDK upload body as a Celery task argument serialized it into the Redis broker queue, so every queued message carried several megabytes and a backlog grew in proportion to payload size. When new .env SDK_PAYLOAD_S3_OFFLOAD is enabled, the sync endpoint now enqueues only an S3 reference and the worker loads the body when the task runs, keeping each broker message at roughly 1 KB. The payload was already being written to S3, so this removes a duplicate copy rather than adding a write.
Note
New SDK_PAYLOAD_S3_OFFLOAD variable is off by default, and nothing changes for deployments that leave it that way.
Workouts now tell you where they came from
Three providers were already sending per-workout provenance and we were dropping it on the floor. Workouts now carry entry_source, intensity and label, so you can finally tell a measured workout from a manually entered one, and see the name the user gave it. Oura, Strava, Garmin and Apple all populate these fields.
Oura MET - movement dynamics throughout the day
Oura sends an intraday MET (metabolic equivalent of task) series alongside daily activity. We now expand that series into individual timestamped samples at the provider's own interval and store it as physical effort, so you get a continuous picture of how intense someone's movement was across the day - not just a daily total. It's also surfaced on the workouts API. Oura is the only provider exposing MET at this granularity today, so this is currently Oura-only.
Multi-gigabyte Apple Health exports, on any storage backend
The Apple Health XML upload limit went from 1 GiB to 5 GiB, with a progress dialog on the frontend. Just as importantly, the flow now works identically against AWS S3 and a self-hosted MinIO - previously the XML path used a hard-coded AWS client and depended on SNS, which MinIO cannot emit. A fully local, AWS-free deployment can now ingest multi-gigabyte Apple exports through the portal.
Security hardening
Four ways to take over an account or a deployment are now closed, all backwards-compatible. API keys are stored hashed and the raw value is shown only once - on create or rotate; the migration hashes existing keys in place, so current integrations keep working. Changing a password now revokes every refresh token the developer holds - previously a leaked token worked forever, since refresh tokens have no expiry. PATCH /developers/{id} is restricted to the caller's own account; before, any team member could set another developer's password. And Garmin webhooks now compare the garmin-client-id header against the configured value instead of just checking that it's present.
Ingestion got substantially faster
We rewrote the DataPointSeries bulk upsert. Instead of chunked INSERT ... VALUES statements carrying an enormous number of bind parameters, a batch is now COPY-ed into a temporary staging table and merged into the target with a single statement. Locally that's a 3.7× speedup on a 50,000-row batch (6.10s → 1.67s). XML imports got their own dedicated Celery queue so they stop competing with everything else.
Multi-arch Docker images
Backend and frontend images now publish for both linux/amd64 and linux/arm64 under the same tags as before.
Garmin backfill controls are going away - ⚠️ deprecation
The Garmin backfill cancel and retry endpoints are removed, and GET status is now deprecated - please stop using it; it will be removed in a future release. This whole mechanism dates back to before we understood how Garmin's backfill API behaves, and it never fit the way other integrations handle historical sync: Garmin allows one request per timeframe per type and answers 409 afterwards, so retry could never do anything useful. The periodic GC task is gone too - it accounted for roughly 15% of worker log lines and in practice never cleared anything, since the per-type timeout and the lock TTL already cover those cases.
What's Changed
Platform
- fix: resolve race condition in token refresh using distributed lock by @sehmbimanvir in #1440
- feat(backend): multipart upload + MinIO parity for Apple's XML imports by @MichalZak17 in #1445
- feat(backend): offload SDK upload payload to S3, enqueue only a reference by @bartmichalak & @KaliszS in #1460
- perf(backend): rewrite DataPointSeries bulk upsert to use COPY + staging merge by @McArthur955 in #1492
- ci: publish multi-arch images via native arm64 runners by @knowald in #1584
Data model
- feat(backend): historical sync status storage by @KaliszS in #1439
- feat:
SyncRuntracking - follow-up by @czajkub in #1448 - feat(backend): add name, intensity and recording source to the workouts API by @McArthur955 in #1510
Security
- fix(auth): revoke developer refresh tokens on password change by @bartmichalak in #1590
- fix(auth): restrict developer update by ID to the caller's own account by @bartmichalak in #1599
- fix(auth): store API keys hashed and show the raw key only once by @bartmichalak in #1592
Scores
Withings
- feat: Withings integration (body, activity, sleep, workouts) - rewrite of #1177 by @bwickner & @czajkub in #1538
Suunto
- feat(integrations): pause-aware stopTime + gear from SummaryExtension by @HugoHeneault in #1043
- fix(integrations): recover missing Suunto sleep windows safely by @bwickner in #1187
- fix(backend): fetch Suunto awake time by @KaliszS in #1525
- feat(backend): Spo2Max & AvgHRV from Suunto sleep by @KaliszS in #1528
Apple
- feat(backend): raise upload limit to 5 GiB and add upload progress dialog for uploading Apple's XML by @MichalZak17 in #1444
Whoop
- fix: Update Whoop workout type mappings by @czajkub in #1451
- fix: split HRV into RMSSD/SDNN and deprecate recovery_score in recovery summaries endpoint by @LouisFerger-Andrews in http...
0.7.0 - Google Health API & SensorBio
Highlights
We encourage you to use our official Docker images:
- backend: https://hub.docker.com/r/themomentum/open-wearables-backend
- frontend: https://hub.docker.com/r/themomentum/open-wearables-frontend
Google Health API & SensorBio
In our latest release, we’re introducing two new integrations with providers. Both integrations will be further developed, but it’s important to note two things:
- The Google Health API replaces the Fitbit API, which is soon to be deprecated by Google
- SenorBio does not currently support webhooks
Admin UI Dashboard improvements
On less efficient infrastructure, the front-end dashboard could take up to several seconds to load data. We optimised this by making improvements to React and removing unnecessary information. In return, we added more details about the volume of stored data and its specific types (see screenshot below). We also improved the display of large numbers (previously, even millions were shown in thousands). The dashboard should now load instantly, even with very large volumes of data and limited RAM.
More information on data sources for the frontend
On the frontend, each piece of data (workouts, sleep sessions, menstrual cycles, activity) will display more information. Until now, we’ve only shown the provider from which the data was saved. Now, the original provider will also be displayed (if applicable – for example, Apple allows data to be synchronised from devices belonging to other providers) along with the device from which the data was retrieved (currently, in the example described earlier, for Apple this will simply be an iPhone, but we also intend to start recording information about the original device soon, provided the providers allow it).
You’ll probably often notice gaps in this information; we intend to rectify them in the next release, as this requires changes to the way data is fetched from the sources.
The option to delete data associated with a selected provider
From now on, the user profile includes an option to delete data from a selected provider. This also automatically disconnects the connection.
SDK applications management in Settings
From now on, you can manage SDK applications via the frontend settings (Admin UI).
Logging improvements
The first change is an improvement to the SDK’s data-saving process. Previously, a single incorrect row would result in the loss of the entire batch. Now, instead, we display the sync status for the problematic rows and log their exact errors to Sentry.
In addition, we have added a lot of new information to the logs, which may be useful for debugging.
HTTP logs from app:
response_body(an error details)request_bytes(how many bytes we are expecting from the payload)error_cause_type(can be helpful to decide whetheran error parsing the bodyresponse_body is caused by connection or decode error)error_cause_msgcontent_type(should beapplication/json)content_encoding(for examplegzip)
{"timestamp": "2026-07-30T17:41:05.671050+00:00", "level": "error", "message": "http_request", "provider": null, "method": "POST", "path": "/api/v1/sdk/users/00000000-0000-0000-0000-0000000000bb/sync", "status": 400, "duration_ms": 17.7, "request_bytes": 3, "response_body": "There was an error parsing the body", "content_type": "application/json", "error_cause_type": "UnicodeDecodeError", "error_cause_msg": "'utf-8' codec can't decode byte 0x80 in position 0: invalid start byte"}
{"timestamp": "2026-07-30T17:41:05.681616+00:00", "level": "error", "message": "http_request", "provider": null, "method": "POST", "path": "/api/v1/sdk/users/00000000-0000-0000-0000-0000000000bb/sync", "status": 400, "duration_ms": 0.6, "request_bytes": 62, "response_body": "There was an error parsing the body", "content_type": "application/json", "content_encoding": "gzip", "error_cause_type": "UnicodeDecodeError", "error_cause_msg": "'utf-8' codec can't decode byte 0x8b in position 1: invalid start byte"}
{"timestamp": "2026-07-30T17:41:05.732281+00:00", "level": "error", "message": "http_request", "provider": null, "method": "POST", "path": "/api/v1/sdk/users/00000000-0000-0000-0000-0000000000bb/sync", "status": 400, "duration_ms": 0.9, "request_bytes": 1000000, "response_body": "There was an error parsing the body", "content_type": "application/json", "error_cause_type": "ClientDisconnect", "error_cause_msg": ""}
Logs from celery worker:
first_error_loc(position of the first broken row in S3 payload)data_type(SDK related, exampleHKQuantityTypeIdentifierWalkingStepLength)success(SDK related, exampletrue)record_count(SDK related, int)task_type(SDK related, exampleforeground)low_power(SDK related, examplefalse)thermal_state(SDK related, examplenominal)types(sync status related, example["basal_energy", "energy", "heart_rate", "steps"])
{"timestamp": "2026-07-30T15:14:21.143378+00:00", "level": "info", "message": "Apple data import completed", "provider": "apple", "action": "apple_sdk_import_complete", "batch_id": "89d47924-a674-48bd-9001-32c3ec3065e2", "user_id": "3c213ef4-38f3-4c0c-89d3-c782a741125d", "incoming_records": 1750, "incoming_workouts": 125, "incoming_sleep": 125, "records_saved": 2023, "workouts_saved": 124, "sleep_saved": 124, "validation_ms": 56.5}
{"timestamp": "2026-07-30T15:14:21.143502+00:00", "level": "warning", "message": "Apple SDK dropped invalid records", "provider": "apple", "action": "apple_sdk_records_dropped", "batch_id": "89d47924-a674-48bd-9001-32c3ec3065e2", "user_id": "3c213ef4-38f3-4c0c-89d3-c782a741125d", "dropped_count": 3, "first_error_loc": "records[0].value", "first_error_msg": "Input should be a valid decimal"}
{"timestamp": "2026-08-11T11:20:50.019232+00:00", "level": "info", "message": "SDK log events received",
"provider": "apple", "action": "sdk_logs_received", "batch_id": "823f86a1-b76a-4f48-....",
"user_id": "d771d1c3-3098-....", "event_count": 2,
"event_types": ["historical_data_type_sync_end", "device_state"], "sdk_version": "0.13.0",
"data_type": "HKQuantityTypeIdentifierWalkingStepLength", "success": true, "record_count": 966,
"task_type": "foreground", "low_power": false, "thermal_state": "nominal"}
{"timestamp": "2026-08-11T11:21:33.975802+00:00", "level": "info", "message": "Sync status",
"provider": "apple", "action": "sync_status", "status": "success", "source": "sdk",
"stage": "completed", "run_id": "a899503a-e7bb-....", "user_id": "d771d1c3-...",
"items_processed": 1439, "types": ["basal_energy", "energy", "heart_rate", "steps"],
"detail": "Apple batch saved"}
What's Changed
Google Health API
SensorBio
Oura
Data model
- perf(backend): remove event_record_detail table by @itsnotashtonkutcher & @KaliszS in #1314
Frontend improvements
- fix(frontend): emit root stylesheet from client build by @aritsyg in #1315
- fix: include icon_url on user connections by @Gasiek in #1347
- feat(backend): add missing Apple and Samsung provider icons by @bartmichalak in #1369
- fix(frontend): load theme colors under Tailwind v4 by @MichalZak17 in #1376
- refactor: optimise dashboard by @KaliszS in #1388
- refactor(frontend): optimise react by @KaliszS in #1389
- refactor: data points count K->M->B->T by @KaliszS in #1390
- feat(frontend): event records in dashboard by @KaliszS in #1391
- refactor(frontend): replace hsl(var(--x)) workarounds with theme utilities by @MichalZak17 in #1397
- refactor(frontend): migrate Tailwind theme to CSS-first @theme b...
0.6.3 - Nap detection & HTTP logs middleware
Highlights
We encourage you to use our official Docker images:
- backend: https://hub.docker.com/r/themomentum/open-wearables-backend
- frontend: https://hub.docker.com/r/themomentum/open-wearables-frontend
Changes to HTTP logs
As HTTP logs usually account for more than 80% of all logs, we have disabled them by default for production deployments and added a new variable to control their flow (ACCESS_LOG_LEVEL). How it works:
ACCESS_LOG_LEVEL=all-> All HTTP logs are available, as beforeACCESS_LOG_LEVEL=errors-> only 4xx & 5xx log entries are availableACCESS_LOG_LEVEL=off-> no HTTP logs- no
ACCESS_LOG_LEVEL(None value):- if
ENVIRONMENT=production->errorslevel is set automatically - else
alllevel is set automatically
- if
We have also changed the structure of the HTTP logs:
- before:
[2026-07-15 11:41:55,669 - uvicorn.access] (INFO) HOSTNAME:PORT - "GET /api/v1/users?page=1&limit=9&sort_by=created_at&sort_order=desc HTTP/1.1" 200
- after:
{"timestamp": "2026-07-15T13:31:03.742767+00:00", "level": "info", "message": "http_request", "provider": null, "method": "OPTIONS", "path": "/api/v1/users?limit=5&sort_by=created_at&sort_order=desc", "status": 200, "duration_ms": 3.5}
Warning
By default, ACCESS_LOG_LEVEL is set to None, so if you have the ENVIRONMENT variable set to production, you will stop logging most messages. If you wish to retain the current behaviour, you must add the ACCESS_LOG_LEVEL variable and set it to all.
Note
For the time being, this change only affects HTTP logs from uvicorn. The new variable does not alter the behaviour of logs from Open Wearables.
Nap detection for Garmin & Oura
We have added support for naps based on data provided by Garmin and Oura. For the time being, this feature works correctly only for these two providers, but we will endeavour to develop our own algorithms to detect naps for providers that do not themselves flag which sleep sessions are naps.
Flag to enable/disable outgoing webhooks
We have added a new variable, OUTGOING_WEBHOOKS_ENABLED, which allows you to enable or disable outgoing webhooks. By default, it is set to false, so you need to add it and set it to true for the webhooks to start working.
Note
Please note that this has nothing to do with incoming webhooks from providers. It only concerns events that Open Wearables sends to client backends.
Strava workout samples
Once the INGEST_WORKOUT_SAMPLES variable is set to true (it was added in previous versions whilst working on Garmin workout samples and is set to false by default), Strava will start logging workout data at a high frequency. You can find out more about this mechanism in the entry “Ingest workout samples” for Garmin in the 0.6.0 release note: https://github.com/the-momentum/open-wearables/releases#release-0.6.0
Redis switched from RDF to AOF mode
We have changed the way Redis works to avoid errors in certain edge cases. You can read the details here: #1188. We have, of course, ensured that the data stored in Redis is preserved during the update.
The Docker Dev image has been reduced in size by around 40%
We achieved this by sealing the .dockerignore file and removing old and unused layers from the Dockerfile. This, of course, only applies to the local development version; nothing changes in the production build.
What's Changed
Outgoing webhooks
- fix(backend): add event emission after reconnection by @itsnotashtonkutcher & @KaliszS in #1262
- feat(backend): silence svix logs at startup by @KaliszS in #1289
- feat(backend): outgoing webhooks enable flag + manual disconnect event by @KaliszS in #1293
Google Health Connect
Oura
- fix(backend): persist daily resting heart rate from sleep data by @andrewle21 & @KaliszS in #1176
- feat: Add nap detection for Oura, Garmin by @czajkub in #1149
Suunto
- fix(integrations): accept float totalDistance in Suunto workout schema by @thecodearrow in #1253
Garmin
- fix(backend): remove dead sleep parameters from Garmin by @KaliszS in #1275
- feat: Add nap detection for Oura, Garmin by @czajkub in #1149
Strava
- feat(backend): Strava workout samples by @Ri087 in #1093
- fix: Strava samples coverage by @czajkub in #1283
Fitbit
API improvements & optimisation
- refactor(backend): update get method to accept string as object_id by @curiousmoonnadin & @KaliszS in #1265
- fix(backend): accept summary_start_time/summary_end_time in provider load_data by @haoyu-haoyu & @KaliszS in #777
- fix(backend): n+1 in sleep summary by @itsnotashtonkutcher & @KaliszS in #1259
- fix: update sleep summary endpoint by @czajkub in #1257
- feat: added Women's Health to Data Coverage tab by @KaliszS in #1285
Observability & deployments
- fix(backend): enable Redis AOF persistence so the svix queue survives restarts by @knowald & KaliszS in #1188
- fix(frontend): remove redundant layers & add .output to .dockerignore by @KaliszS in #1296
- feat(backend): middleware for uvicorn logs by @KaliszS in #1297
- feat(backend): timestamp for HTTP middleware logs by @KaliszS in #1301
MCP
Docs & chores
- feat: automatic update for coverage docs by @KaliszS in #1286
- docs: remove custom deployment note from Garmin warning by @bartmichalak in #1254
- chore: update frontend dependencies by @KaliszS in #1270
- chore: update backend dependencies by @KaliszS in #1271
- docs: sync .env.example with config.py + CodeRabbit drift check by @aks129 in #1264
- docs: remove custom deployment note from Garmin warning by @bartmichalak in #1272
- docs: remove Garmin API integration page from nav menu by @bartmichalak in #1273
- chore(frontend): update ts & prettier by @KaliszS in #1284
- docs: clear out roadmap page, coming back soon by @bartmichalak in #1294
- chore: update Open Wearables to 0.6.3 by @KaliszS in #1302
New Contributors
- @aks129 made their first contribution in #1264
- @curiousmoonnadin made their first contribution in #1265
- @itsnotashtonkutcher made their first contribution in #1262
- @thecodearrow made their first contribution in #1253
- @andrewle21 made their first contribution in #1176
- @Ri087 made their first contribution in #1093
Full Changelog: 0.6.2...0.6.3
0.6.2 - Data integrity
Highlights
Counting steps and energy
We have updated the summaries for step count and energy (kcal). The data in the data_point_series table now includes a new is_daily_total column. This is because data providers supply data at varying levels of granularity. Previously, an error occurred during aggregation when some providers sent a daily entry alongside granular entries (e.g. Garmin in 15-minute chunks). From now on, these will be correctly flagged via the new column. The current logic for displaying totals in summaries means that the daily record will be used first. Only if this does not exist will all other entries from that day be summed.
Warning
The next time Open Wearables is launched, an automated script will set the value to true in a new column for the relevant rows in the data points. Intraday aggregates are not flagged as false by the script, so as not to place too much load on the application at start-up (although all new records will be). Therefore, the value none in the is_daily_total column should be treated as equivalent to false.
Active minutes
From now on, active_minutes will be treated as a separate series type and retrieved directly from all providers that supply this data. Until now, we have defined active minutes as the number of minutes in which more than 30 steps were recorded. This definition will remain the fallback for providers who do not supply this data. Our science team is developing a new, improved method for calculating this parameter, which will be implemented in the near future.
Warning
This change will not affect any data that has already been saved to the database. To retrieve correct active minutes for this data, you will need to resynchronise it. With Garmin, you must first delete the data, whereas with other providers, it will be overwritten during historical synchronisation.
Tip
Providers that directly support active minutes include: Garmin, Oura, Polar and Ultrahuman.
Filter sleep sessions by priority
Important
One API endpoint (/users/{user_id}/events/sleep) has been updated. However, this is a backwards-compatible change as a new parameter has been added that is set by default to not alter the existing behaviour of the endpoint.
We have added an optional filter_by_priority flag to the /events/sleep endpoint. This flag is disabled by default and, when enabled, filters out duplicate sleep sessions. On our frontend, this option is available as a toggle, as shown in the screenshot below. The screenshot illustrates a situation in which Oura connects to both Apple Health and Open Wearables, meaning sleep sessions are sent to the database directly from Oura and via Apple Health.
Garmin's respiratory rate
We have corrected the way this parameter is retrieved. Until now, we had been searching for it in raw payloads under the wrong name, meaning it was not being saved to the database.
Improved data summary per user
We have enhanced the visual design of the data summary section and added time filters, enabling you to filter your data summaries by day or by a specified time period.
Manual invitation links
Thanks to our contributor, we can now manually copy invitation links, which is useful if you don't have an email account configured.
connection.revoked outgoing webhook
A new event is now available in outgoing webhooks, which will notify you when connections with providers are revoked.
What's Changed
Data integrity
- fix(backend): take daily data OR sum intraday data by @KaliszS in #1232
- fix(backend): active minutes as a new series type by @KaliszS in #1242
- fix: correctly filter sleep sessions by priority on frontend by @czajkub in #1249
Oura
Apple Health
Garmin
Other Improvements
- fix: Mark connection as revoked on failed token refresh + connection.revoked outgoing webhook by @czajkub in #1203
- fix(backend): split iOS & Android coverage by @KaliszS in #1233
- feat: support manual invite links when email is not configured by @edsammy in #1230
- fix(frontend): optimise activity tab by @KaliszS in #1245
- refactor(frontend): improve data summary per user by @KaliszS in #1246
Docs & chores
- chore: update backend dependencies by @KaliszS in #1221
- chore: disallow indexing of docs Next.js and Vercel internal paths by @psobusiak in #1224
- docs: update Strava integration docs to reflect API terms changes by @bartmichalak in #1234
- docs: update resilience score by @anjazych in #1115
- chore: update Open Wearables version to 0.6.2 by @KaliszS in #1247
New Contributors
Full Changelog: 0.6.1...0.6.2
0.6.1 - Data Coverage & Observability
Highlights
Data Coverage tab
We have added a new Data Coverage tab to the admin panel which shows the data that we support for each provider. This information is always up to date.
Note
Although it includes OpenWearables capabilities, it does not display which data has already been synchronised with the system.
Observability, deployments & logs
The focus of this release was on making debugging and troubleshooting easier by improving the visibility of logs and sync statuses. These changes include the following:
- adding a workflow to publish an image to Docker Hub
- linked webhook logs to users and/or traces (including failed attempts to receive a webhook).
- the application version has been added to Sentry logs
- improved reliability when saving sync statuses from webhooks to Redis (new
skippedstatus added). - new metadata has been added to sync statuses, dividing new items into
insertedandupdated.
Lookback for pull syncs
Although we recommend using webhooks, we have added lookbacks as a safeguard against data gaps if you need to use periodic pull syncs for any reason. This could happen if a user has not opened their wearables app for a long time, causing the live sync window to expire. This issue can now be resolved by syncing data from a specified number of minutes, hours or days in the past.
In order to use this feature, you will need to set the PULL_SYNC_LOOKBACK environment variable, specifying a string containing a number and a time unit. For example, 2d would mean 2 days, 3h would mean 3 hours, and 15m would mean 15 minutes. In periodic pull mode, live sync by default retrieves data recorded from the start of the day on which it is called until the present moment. Setting the lookback extends the start of the sync window by the specified duration and unit of time.
Note
PULL_SYNC_LOOKBACK is disabled by default.
This session is in committed state error
Thanks to our new contributor, the bug that caused ORM objects to be downgraded has been fixed.
What's Changed
Observability, deployments & logs
- ci: add manual Docker Hub image publish workflow by @kczpl in #1179
- fix(backend): build data source display name from reloaded provider strings by @knowald in #1154
- chore(backend): add user/trace correlation to webhook logs by @bartmichalak in #1183
- feat(backend): set Sentry release from version + commit SHA by @bartmichalak in #1196
- docs: remove publishing the images section by @bartmichalak in #1205
- fix(backend): webhooks syncs being saved into sync logs by @KaliszS in #1209
- refactor: improved sync metadata logging by @KaliszS in #1211
- fix(integrations): drop non-retriable 4xx webhook deliveries instead of retrying forever by @slavarosin in #1053
Docs & chores
- chore(backend): replace Garmin provider icon with brand-compliant logo by @bartmichalak in #1210
- chore: update OW version to 0.6.1 by @KaliszS in #1216
- chore: update frontend dependencies by @KaliszS in #1217
- chore: update backend dependencies by @KaliszS in #1218
Suunto
- fix(backend): report accurate Suunto sleep sync stats, skip unsaveable sessions by @bwickner in #1182
- fix(integrations): handle Suunto workout webhook edge cases by @HugoHeneault in #1042
- fix(backend): suunto tests by @KaliszS in #1189
Garmin
- fix(backend): persist body composition muscle mass from Garmin webhooks by @bartmichalak in #1194
- fix(backend): persist blood pressure from Garmin webhooks by @bartmichalak in #1193
Polar
- fix: accept float values for Polar training load fields (Fixes #1198) by @rtmalikian in #1204
Whoop
Fitbit
- fix(integrations): coerce Fitbit string start/end dates to datetime before strftime by @slavarosin in #1054
Other improvements
- feat: automatic data types coverage tab by @KaliszS in #1206
- feat(backend): lookback in pull sync by @KaliszS in #1215
- fix(frontend): empty-state hint for provider priorities list by @bartmichalak in #1191
- fix(webhooks): fire event record webhook directly instead of via after_commit by @zacharydrillings-atria in #1208
New Contributors
- @bwickner made their first contribution in #1182
- @rtmalikian made their first contribution in #1204
- @zacharydrillings-atria made their first contribution in #1208
Full Changelog: 0.6.0...0.6.1
0.6.0 - Women's Health & FIT files (Garmin)
Highlights
Women's Health
We now support menstrual cycle tracking for Garmin wearables. The new women's health tab is only available if the user has fetched any related data. Otherwise, it is invisible.
Ingest workout samples (high data granularity)
We have added an option to enable second-by-second granularity for certain data from Garmin, such as heart rate, speed, cadence, power, latitude, longitude, elevation and running dynamics (which require a FIT file), as well as air temperature (also requires FIT file to be available).
Caution
This option is disabled by default. To enable it, you must set the INGEST_WORKOUT_SAMPLES environment variable to true.
Warning
According to our tests, this will increase database memory usage by approximately 170 MB per user per month.
FIT file parser and saving to S3
A parser for FIT files has been implemented. Currently, there is no option to upload these files manually; they are processed automatically via Garmin webhooks. However, the same parser will eventually support other providers.
Garmin currently extracts the following data from FIT files and saves it to the database:
- training segments (laps, swim lengths, etc.)
- time spent in each heart rate zone during a workout
- time spent in each power zone during a workout
- air temperature (
INGEST_WORKOUT_SAMPLESrequired) - running vertical oscillation (
INGEST_WORKOUT_SAMPLESrequired) - running vertical ratio (
INGEST_WORKOUT_SAMPLESrequired) - running stride length (
INGEST_WORKOUT_SAMPLESrequired) - running ground contact time (
INGEST_WORKOUT_SAMPLESrequired) - running stance time balance (
INGEST_WORKOUT_SAMPLESrequired)
Note
We have added a new environment variable called STORE_FIT_FILES, which is set to false by default. When this is set to true, raw FIT files from Garmin will be saved to a previously configured S3 bucket. See the S3 configuration guide for more information: https://openwearables.io/docs/dev-guides/raw-payload-storage
Warning
Storing raw FIT files in S3 results in an additional data usage of between a few MB and a dozen or so MB per user per month.
Many Oura bugs fixed
We have fixed several bugs relating to the Oura integration. We have added time zone offset saving to the database, supported sleep summary metrics, improved data retrieval for current day summaries and migrated HRV from SDNN to RMSSD. Existing SDNN values in the database will be migrated by an automatic script that runs when the application starts.
Added redis TLS support
To enable TLS support (rediss://), set the new environment variable REDIS_SSL to true.
The day prefix for sleep start times that began before midnight
From now on, the start time of a sleep session in the sleep section will be prefixed with the previous day’s name if it was before midnight. If a sleep session ends after midnight, it will be displayed under the following day in the admin UI (as it always has been). Please note that this is dynamic and will adjust to the user’s time zone.
Support for synchronising the same data across multiple accounts
We have added dedicated support for users who need to synchronise their devices with several Open Wearables accounts for testing purposes. No configuration is required to use this feature. Simply pair the relevant accounts with your wearables account and synchronise any of them. A badge will appear in each account to indicate which accounts are linked, along with the relevant sync statuses. This shows that, for a given account, the source of the currently syncing data is a linked Open Wearables account. This means that, instead of repeatedly syncing the same data - which is virtually impossible with Garmin during backfilling, for example - it will be synced once and copied to the other accounts. More information: https://openwearables.io/docs/architecture/multi-account-sync
What's Changed
Garmin FIT files & high granularity data support
- feat(backend): add migration with new jsonb fields for workout_details by @KaliszS in #1079
- feat(backend): add average_cadence support for Garmin by @KaliszS in #1080
- feat(backend): Garmin workout samples by @KaliszS in #1082
- feat(backend): FIT files parser by @KaliszS in #1096
- fix(backend): fit map stride_length -> step_length by @KaliszS in #1097
- feat(backend): Garmin mappings for elevation, GPS & running dynamics + optional saving FIT files to s3 by @KaliszS in #1099
- fix(backend): stop storing Garmin average HR as heart_rate_min by @knowald in #1121
- feat(backend): Garmin support for workout segments (laps, swim lengths etc) by @KaliszS in #1118
- fix(backend): null workout heart_rate_min rows copied from average HR by @knowald in #1133
- feat(backend): HR & Power Zones [Garmin] by @KaliszS in #1175
Garmin Women's Health
Oura bugs
- fix: Oura missed commit and sleep summary metrics by @czajkub in #1086
- feat(backend): Oura zone_offset extraction by @KaliszS in #1168
- fix(backend): polling logic for Oura drops the current day for daily metrics by @bartmichalak in #1162
- fix(backend): store Oura HRV as RMSSD instead of SDNN by @bartmichalak and @KaliszS in #1100
Apple Health bugs
- fix(backend): widen data_source.source to VARCHAR(100) by @chrisedington in #1103
Suunto bugs
- feat(integrations): emit resting_heart_rate series from sleep HRMin by @HugoHeneault in #1055
Polar bugs
Infrastructure improvements
- fix: mount redis volume at /data so persistence works by @knowald in #1127
- feat(backend): support TLS (rediss://) for Redis connections by @kczpl in #1134
Other improvements
- fix(frontend): stop activity & sleep dates shifting a day for users west of UTC by @bartmichalak and @KaliszS in #1160
- refactor: fixed scopes db saving & improved UI by @KaliszS in #1088
- feat: Add webhook management endpoints by @czajkub in #1049
- fix: update last_synced_at on webhook receive by @czajkub in #1145
- feat: sharable syncs (the same data) between different OW users by @KaliszS in #1147
- feat(frontend): OpenWearables version visible under logout button by @KaliszS in #1165
Docs & chores
- fix(docs): update GitHub repository links in iOS SDK documentation by @bartmichalak in #1064
- docs: update available providers in readme by @bartmichalak in #1069
- chore: bump the npm-minor-patch group across 1 directory with 4 updates by @dependabot[bot] in #1057
- chore: update sentry-sdk[fastapi] requirement from >=2.59.0 to >=2.60.0 in /backend by @dependabot[bot] in #1061
- refactor(backend): update deps (redis v8) by @KaliszS in #1087
- chore: bump the npm-minor-patch group in /frontend with 13 updates by @dependabot[bot] in #1102
- docs(mcp): remove formatted duration fields absent from sleep tool output by @knowald in #1122
- docs: clarify migrations auto-run on deploy temporarily by @bartmichalak in #1129
- docs: note developer program is on hold by @bartmichalak in #1130
- docs: drop "| Open Wearables" suffix from page titles by @bartmichalak in #1131
- docs: add Fitbit and Google Health Connect to the coverage matrix by @knowald in #11...
0.5.2 - Polar refactor
Highlights
Polar got refactor & webhook support
Polar now synchronises sleep and timeseries data. By default, the Periodic Pullsync method is enabled, but webhooks can be activated in Settings > Providers.
Expanded Sport Type Support across Multi-Provider Mappings
Expanded the WorkoutType enum to explicitly support specific activities that were previously bundled into WorkoutType.OTHER. Over 70 provider-specific mappings across Apple, Garmin, Suunto, and WHOOP have been updated. New explicitly supported sports include: Cricket, lacrosse, racquetball, bowling, archery, fencing, fishing, wakeboarding, parkour, curling, and more.
Garmin fetches new fields
allDayRespiration and moveIQActivities have been added to the list of Garmin-specific attributes that we support.
Strict RMSSD enforcement for HRV-CV Resilience Score
Temporarily disabled the automatic fallback to SDNN when overnight RMSSD data is missing within the sleep window. Until SDNN-based calculations are fully validated, users without RMSSD data will now receive a null score instead of a potentially misleading fallback value.
What's Changed
- refactor(backend): updated all python dependencies by @KaliszS in #1023
- refactor(frontend): updated all TypeScript dependencies by @KaliszS in #1025
- fix(backend): garmin allDayRespiration & moveIQActivities by @KaliszS in #1026
- feat(backend): add recovery summary endpoint by @KaliszS in #1027
- chore: disable SDNN fallback in HRV-CV score calculation pending validation by @FBegiello in #1040
- fix: commit each data type individually and rollback session on exception for Oura and Whoop by @czajkub in #1022
- fix: Cast to local date for summary endpoints by @czajkub in #1033
- fix(backend): validation for date parameters in OpenAPI specs by @KaliszS in #1052
- fix(users): skip invalid user rows during email validation by @zxjt2z42px-creator in #1032
- fix: Handle duplicate EventRecordDetail inserts gracefully with savepoint by @czajkub in #1047
- refactor(frontend): update dependencies by @KaliszS in #1056
- fix: Add missing workout types, rename series type to SDK by @czajkub in #1048
- feat: Polar refactor by @czajkub in #1041
- fix(integrations): emit outgoing webhook for suunto live workout push by @HugoHeneault in #1045
New Contributors
- @zxjt2z42px-creator made their first contribution in #1032
- @HugoHeneault made their first contribution in #1045
Full Changelog: 0.5.1...0.5.2
0.5.1 - Oura & Strava Webhooks
Highlights
Oura & Strava got webhook support
By default Periodic pull only is enabled, but webhooks can be activated in Settings > Providers.
Live sync status is now available
It's already avilable in our Admin Panel (there is global Syncs tab, but all of statues are also visible in user profiles).
If you need to integrate this feature with your backend, checkout our integration guide: https://openwearables.io/docs/api-reference/guides/sync-status-stream
Sync statuses are also available as outgoing webhook events. This is the preferred solution if you need to permanently store this data, because we only store it in Redis for 24 hours.
New Admin Panel Dashboard design
RMSSD HRV is now included in Body Summary
What's Changed
- chore: bump typescript from 5.9.3 to 6.0.2 in /frontend by @dependabot[bot] in #993
- fix: add hrv rmssd to body summary by @czajkub in #949
- refactor: migrate all recovery scores from
data_point_seriestohealth_scoreby @czajkub in #950 - refactor: polish Oura webhooks and adjust them to new webhook pattern by @czajkub in #990
- refactor(frontend): Dashboard v2 by @KaliszS in #1003
- feat: sync status (fastapi SSE) by @KaliszS in #986
- feat: Sleep score recalculation after sleep session merge by @czajkub in #884
- refactor(frontend): Dashboard redesigned by @KaliszS in #1015
- fix(backend): scope x100 ratio->percent conversion to Apple provider only by @kjsteuer in #917
- refactor: Strava webhooks by @czajkub in #1010
Full Changelog: 0.5.0...0.5.1




