---
title: "Open Wearables 0.8.0: Withings & Sync History"
description: "Open Wearables 0.8.0 adds Withings, historical sync tracking, 3.7x faster ingestion, 5 GiB Apple Health imports, and four security fixes."
canonical_url: "https://openwearables.io/blog/open-wearables-0-8-0-withings-sync-history"
---

# Open Wearables 0.8.0: Withings, sync history, and faster ingestion

[All posts](https://openwearables.io/blog)

[Releases](https://openwearables.io/blog/category/releases)

Open Wearables Team · Sep 15, 2026 · 10 min read

Open Wearables 0.8.0 is available. This release adds Withings, a `sync_run` tracking table with two new history endpoints, a rewritten bulk upsert path that benchmarks 3.7x faster, 5 GiB Apple Health XML uploads with MinIO parity, four account takeover fixes, and multi-arch Docker images.

Two things need attention before you upgrade: a manual migration check if you track `nightly`, and the removal of the Garmin backfill cancel and retry endpoints. Both are covered at the bottom.

## Key Takeaways

- **New: Withings integration.** OAuth 2.0, polling the Withings Measure and Sleep APIs by default. Body, activity, sleep, and workouts. Webhooks are optional and created per user, not per application.
- **New: `sync_run` and `sync_run_data_type` tables.** One row per data type per run, across provider pulls and pushes, SDK uploads, SDK logs, Apple XML imports, and Garmin backfills. Exposed via `GET /users/{id}/sync/history` and `GET /sync/history/{run_key}`. Historical syncs only and API-only in this release.
- **Bulk upsert rewritten** to COPY into a staging table and merge: 3.7x faster on a 50,000-row batch locally, 6.10s down to 1.67s. XML imports moved to a dedicated Celery queue.
- **Apple Health XML uploads raised to 5 GiB**, with S3 and MinIO parity. The hard-coded AWS client and SNS dependency are gone, so AWS-free deployments can ingest multi-gigabyte exports.
- **Optional SDK payload offload** via `SDK_PAYLOAD_S3_OFFLOAD`: enqueue an S3 reference instead of the body, keeping broker messages at roughly 1 KB. Off by default.
- **Four account takeover fixes**, all backwards-compatible: hashed API keys, refresh token revocation on password change, `PATCH /developers/{id}` scoped to the caller, and `garmin-client-id` header verification.
- **Workout provenance** (`entry_source`, `intensity`, `label`) from Oura, Strava, Garmin, and Apple. **New series:** Oura intraday MET, previously not stored at all, now parsed into timestamped samples as `physical effort`.
- **Multi-arch images** for `linux/amd64` and `linux/arm64` under the same tags.
- **Removed:** Garmin backfill cancel and retry endpoints. **Deprecated:** Garmin backfill `GET status`, and Whoop `recovery_score` in the recovery summaries endpoint.

## Withings

Withings is now a supported provider. All data categories are available straight away, along with the methods for integrating them: body, activity, sleep, and workouts.

The integration introduces new elements to the platform's architecture. Per the provider docs, auth is OAuth 2.0 with comma-separated scopes, and the default sync path polls the Withings Measure and Sleep APIs. Webhook push is optional: subscriptions are created per user with that user's own token rather than once per application, and an application without compliant webhooks is capped by Withings at 10 linked users. The application-wide rate limit is 120 requests per minute.

Not supported: sleep stage timeline (hypnogram), ECG, and HRV.

## Sync History

Sync completion status was already tracked in Redis, with a 24-hour retention window. This release moves it into the database.

Three problems, all specific to historical syncs:

- **Redis is not durable.** After 24 hours the record is gone.
- **No completion signal on long runs.** The Redis status was written when a sync *started*, not when it finished. A historical sync covering a year of Apple data runs for a long time, and there was no way to distinguish one still in progress from one that died. The table now records when a historical sync ended.
- **No resume point after a drop.** When a historical sync broke partway through a large range, nothing recorded how far it had got. The per-data-type rows now show where it stopped, so it can be continued from that range instead of restarted.

A new `sync_run` table stores provider, scope, status, timing, item counts, and errors for each run. A companion `sync_run_data_type` table gives one row per data type per run, so a sync that succeeded on sleep and failed on workouts records both outcomes separately instead of an opaque blob.

**Live syncs are deliberately not persisted.** The data model supports them, but writing every live sync would flood the table. Historical syncs are the case where durability and resumability matter.

A stale sweep closes runs that stopped reporting. It checks Redis for liveness first, so a slow backfill is not cut off prematurely.

Tracking covers these ingestion paths:

- Provider pulls and pushes
- SDK uploads
- SDK logs
- Apple Health XML imports
- Garmin backfills

Two endpoints expose the data:

```
GET /users/{id}/sync/history
GET /sync/history/{run_key}
```

Use the first to answer "what happened for this user and when," the second to inspect a specific run by key.

This is API-only in 0.8.0. The frontend picks it up in its next iteration.

## 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. Large uploads meant multi-megabyte broker messages.

With the new `SDK_PAYLOAD_S3_OFFLOAD` env variable enabled, the sync endpoint enqueues only an S3 reference and the worker loads the body when the task runs. Each broker message stays at roughly 1 KB.

The payload was already being written to S3, so this removes a duplicate copy rather than adding a write.

**Off by default.** Nothing changes for deployments that leave it that way.

## Bulk Upsert Rewritten

The `DataPointSeries` bulk upsert previously issued 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 is a 3.7x speedup on a 50,000-row batch: 6.10s down to 1.67s.

XML imports also moved to a dedicated Celery queue, so large Apple exports no longer compete with routine provider syncs for worker capacity.

## Apple Health XML: 5 GiB and MinIO Parity

The Apple Health XML upload limit went from 1 GiB to 5 GiB, with a progress dialog on the frontend.

The multipart upload flow now works identically against AWS S3 and 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.

## Workout Provenance

Three providers were already sending per-workout provenance and the platform was discarding it. Workouts now carry three fields:

- `entry_source`
- `intensity`
- `label`

Populated by Oura, Strava, Garmin, and Apple. These let you tell a measured workout from a manually entered one, and see the name the user gave it.

## Oura Intraday MET

**This is a new series, not a granularity change.** Oura MET was not stored at all before this release, so nothing you already have is affected and there is no daily-to-intraday conversion.

Oura sends an intraday MET (metabolic equivalent of task) series alongside daily activity. It is now parsed into individual timestamped samples at the provider's own interval and stored as `physical effort`, giving a continuous picture of movement intensity across the day.

Oura is the only provider exposing MET at this granularity today, so this is currently Oura-only.

Also in this release: the invalid `activity` OAuth scope has been removed from the Oura defaults. Parsed Oura MET data is exposed through the workouts API.

## Security

Four ways to take over an account or a deployment are now closed. All fixes are backwards-compatible.

**API keys are stored hashed.** 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 revokes every refresh token** the developer holds. Refresh tokens have no expiry, so a leaked token previously worked forever.

**`PATCH /developers/{id}` is restricted to the caller's own account.** Before, any team member could set another developer's password.

**Garmin webhooks compare the `garmin-client-id` header** against the configured value, instead of just checking that it is present.

A `SECURITY.md` vulnerability reporting policy has been added to the repo.

## Multi-Arch Docker Images

Backend and frontend images now publish for both `linux/amd64` and `linux/arm64` under the same tags as before. They are built via native arm64 runners.

Recommended images:

- `themomentum/open-wearables-backend`
- `themomentum/open-wearables-frontend`

Tag scheme:

- `latest`: newest stable image
- `0.8.0`, `0.8`: pinned stable release
- `nightly`: unstable, rebuilt nightly from main

## Provider Fixes

**Suunto:** pause-aware `stopTime` and gear from `SummaryExtension`; missing sleep windows recovered safely; awake time fetching implemented; SpO2Max and AvgHRV extracted from sleep.

**Whoop:** `/v2/cycle` endpoint support added; HRV split into RMSSD and SDNN with `recovery_score` deprecated in the recovery summaries endpoint; recovery endpoint fixed and `cycle_id` resolved for `recovery.updated`; workout type mappings updated and additional sport names mapped.

**Ultrahuman:** real synced item counts now reported; skin temperature stored under the `skin_temperature` series; granular sleep stages parsed from `sleep_graph`.

**Garmin:** webhook `garmin-client-id` verification; backfill GC task, cancel and retry endpoints removed.

## Other Changes

**Token refresh race condition** resolved using a distributed lock.

**SDK connections:** `connection.created` and `connection.revoked` events are now emitted. SDK tokens can disconnect their own connections. "Sync SDK" is renamed to "Mobile SDK."

**Outgoing webhooks:** `connection.created` and `connection.revoked` emitted; sleep data delivery unified between API and webhook; webhook sent after seeding sleep data; webhook event filters can now be removed.

**Scores:** duplicate sleep score calculation prevented.

**MCP:** `get_menstrual_cycles` tool added. FastMCP upgraded from 2 to 3.

**Admin panel:** metric descriptions with tooltip hints on the coverage matrix; responsive sidebar on mobile; Tanstack menu position fixed; "Device info not available" label cleared when the source has no device details.

**Platform:** DB connection released before the SSE stream starts; `GET /users` improved; `handle_duplicates` decorator removed from `event_record_detail_service`; old data migrations removed from startup; migration chain guarded against re-pointed migrations; admin seed clarified to run only on an empty developer table.

**Docs:** production compose setup replaced with a Docker deployment guide; provider and webhook docs refreshed; Historical Data Availability page added for providers; developer portal settings and access model documented; structured logging documented as the backend default; Samsung Health production requirements documented; data migrations moved to Developer Guides; contributing guidelines and PR template rewritten.

## Deprecations and Removals

**Removed:** Garmin backfill cancel and retry endpoints. 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.

**Deprecated:** Garmin backfill `GET status`. Stop using it; it will be removed in a future release.

**Deprecated:** Whoop `recovery_score` in the recovery summaries endpoint, superseded by the RMSSD and SDNN split.

## Upgrading

```
docker compose pull
docker compose up -d
```

Migrations run automatically on startup.

**Nightly users, read this.** If you updated between 2026-09-07T17:46:43Z and 2026-09-08T14:55:55Z, you may have pulled a version containing a faulty migration. Check whether your `workout_details` table has the `entry_source`, `intensity`, and `label` columns. If not, add them manually:

```
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);
```

API keys are hashed by a migration that runs in place, so existing integrations keep working without action. From this release onward, a newly created or rotated key is shown once and cannot be read back afterwards.

Full changelog: [github.com/the-momentum/open-wearables/releases/tag/0.8.0](https://github.com/the-momentum/open-wearables/releases/tag/0.8.0)

## Frequently Asked Questions

### Do I need to do anything about the hashed API keys before upgrading?

No. The migration hashes existing keys in place, so current integrations keep working. The change applies going forward: a key created or rotated after this release is displayed once and cannot be read back afterwards.

### Why is my Withings integration capped at 10 users?

That cap comes from Withings, not Open Wearables. Withings creates notification subscriptions per user rather than once per application, and applications without compliant webhooks are limited to 10 linked users. Webhook endpoints must use HTTPS on a public hostname, ports 80 or 443 only. The default sync path polls the Measure and Sleep APIs, so polling works without the webhook setup.

### Does Withings give me sleep stages and HRV?

Sleep totals yes, including duration, efficiency, and deep, light, REM and awake totals. The sleep stage timeline (hypnogram) is not supported, and neither are ECG or HRV.

### Should I turn on `SDK_PAYLOAD_S3_OFFLOAD`?

Only if SDK upload volume is putting pressure on your Redis broker. It is off by default and nothing changes for deployments that leave it that way. The payload was already being written to S3, so enabling it removes a duplicate copy rather than adding a write.

### Where do I see sync history in the UI?

Nowhere yet. 0.8.0 exposes it through the API only. The frontend picks it up in its next iteration.

### I used the Garmin backfill retry endpoint. What replaces it?

Nothing, and nothing needs to. Garmin allows one backfill request per timeframe per type and answers 409 afterwards, so retry could never do anything useful. The per-type timeout and lock TTL cover the cases the removed controls were handling. `GET status` still works but is deprecated.

### Is the 3.7x ingestion speedup something I need to configure?

No. The bulk upsert rewrite applies automatically. The dedicated Celery queue for XML imports is part of the same change.

### My Whoop integration reads `recovery_score`. Is it gone?

Not gone, deprecated in the recovery summaries endpoint. HRV is now split into RMSSD and SDNN as distinct metrics, and those are what to build on going forward.

Questions or feedback? Open an issue on GitHub or join the Discord community.

## Keep reading

Releases · Oct 26, 2026 · 11 min read

### Open Wearables 0.10.0: new dashboard, nutrition data and sync-aware webhooks

Open Wearables 0.10.0 rebuilds the dashboard in SvelteKit, adds 42 nutrition series types with a meal event, tags every data webhook with the sync it came from and splits calories into total, active and basal energy. Contains breaking changes.

Releases · Sep 23, 2026 · 13 min read

### Open Wearables 0.9.0: Google split into two providers, energy renamed, and a long list of API corrections

Open Wearables 0.9.0 splits Google into google_health and health_connect, renames energy to active_energy, disables the Google Health API rollup that wrote duplicated rows, and fixes date bounds, filters and pagination across the API. Contains breaking changes.

Releases · Aug 13, 2026 · 7 min read

### Open Wearables 0.7.0: Google Health API, Sensor Bio, and a faster Admin UI

Open Wearables 0.7.0 adds a Google Health API integration ahead of the Fitbit Web API sunset, a new Sensor Bio provider, finer Oura sleep granularity, and Admin UI and logging improvements.

## Run it in your own cloud

Open Wearables is open source and self-hosted. Start from the repository, or bring us in to stand it up in your account.

[Get started](https://openwearables.io/docs/quickstart) [Book a demo](https://openwearables.io/book)
