Releasesv0.1.0

Consent

Per user, per source and per data category: what is stored, revocation and its purge.

Open Health Sync keeps each end user's consent per source and per data category: a user can share their sleep and activity from one wearable but not their heart data, and change their mind later. The engine enforces it. Records of a category the user has not granted are dropped before they are stored, and the records already stored in a category the user revokes are deleted. Every change is kept in a history with who made it and what backs it, and announced by webhook.

Your app collects the consent (a screen, a form) and tells Open Health Sync what the user chose. Open Health Sync does not show consent screens itself.

Categories

CategoryWhat it covers
activitySteps, distances, energy, floors, workouts, daily summaries, speed and power
locationWorkout GPS routes
heartHeart rate, resting and walking heart rate, HRV, RR intervals, rhythm events
ecgElectrocardiograms and their voltage samples
blood_pressureSystolic and diastolic pressure
glucoseBlood glucose (CGM included), insulin delivery
respiratoryRespiratory rate, SpO2, VO2 max, lung function, inhaler use
bodyWeight, height, body composition, body and skin temperature
sleepSleep sessions, phases and scores
nutritionFood, drinks, water, caffeine
reproductiveMenstrual cycle, ovulation, basal body temperature, sexual activity, pregnancy
mentalMindfulness, state of mind, mood, stress
otherAnything no category above describes

A record goes with its parent: route points are location but also go when activity is not granted (no workout, no route), and sleep phases go with their session. GET /v1/users/{id}/consents returns the list of categories, so your consent screen can follow it.

How it is enforced

  • No consent recorded for a source: everything from it is stored. This keeps an existing install working as before.
  • OHS_CONSENT_REQUIRED=true: a source with no consent recorded stores nothing. Set it when your app always asks for consent before connecting a source.
  • Consent recorded: only the granted categories are stored. A category left out of the first consent counts as not granted.
  • Revocation: from the moment it commits, new records of the category are dropped. The records already stored from that source are deleted by the etl role in the background (usually within seconds). Granting the category again before the purge runs keeps the data.
  • Raw payloads (the files the engine keeps as received) are not split by category: they follow OHS_RETENTION_DAYS and are deleted with the user or with the provider's data. A reprocess of a raw file applies the consent current at that time.

Withheld records are not failures: they are counted in the ETL log line records withheld, no consent for their category, never written to failed_records.

Routes

All take a tenant API key.

# Grant heart and sleep from Oura; every other category of Oura is revoked.
curl -X PUT "$OHS_URL/v1/users/$USER_ID/consents/oura" \
  -H "Authorization: Bearer $OHS_KEY" -H "Content-Type: application/json" \
  -d '{"granted":["heart","sleep"],"evidence":"consent screen v3, 2026-10-13"}'

# The user's consent for every source, and the list of categories.
curl "$OHS_URL/v1/users/$USER_ID/consents" -H "Authorization: Bearer $OHS_KEY"

# Every change, newest first (paged with cursor and limit).
curl "$OHS_URL/v1/users/$USER_ID/consents/history" -H "Authorization: Bearer $OHS_KEY"

# Revoke everything from Oura.
curl -X DELETE "$OHS_URL/v1/users/$USER_ID/consents/oura?evidence=withdrawn%20in%20settings" \
  -H "Authorization: Bearer $OHS_KEY"

PUT takes the whole list: granted is required ([] for none) and evidence is optional, up to 500 characters (a form version, a ticket number). The answer is the source's consent after the change: each category with granted, granted_at and revoked_at.

A history event holds the source, the category, granted or revoked, the time, the id of the API key that made the change and the evidence. Setting the same consent twice records nothing. The audit log records each call as consent.read or consent.write (audit-log.md).

Webhooks

TypeSent whendata
consent.grantedOne or more categories of a source were granteduser_id, external_ref, provider, categories
consent.revokedOne or more categories of a source were revokeduser_id, external_ref, provider, categories

A first consent announces its grants only: the categories it leaves out were never granted. See webhooks.md for signatures and retries.

Erasure

Consent, its history and queued purges belong to the user: they are deleted with the user (DELETE /v1/users/{id}) and, for one source, with that source's data (a disconnect with delete_data=true). The audit log keeps the record that the changes happened.

Edit on GitHub

On this page