Sonar processes raw wearable data through three distinct layers before it reaches your API calls. Understanding this model explains how data from different devices stays consistent and why you never deal with raw sensor output.

The Three Layers

RawDevice-native format, original unitsNormalizedUnified schema, consistent units, per deviceCanonicalBest value per metric, deduplicated NormalizeDeduplicate + select RawDevice-native format, original unitsNormalizedUnified schema, consistent units, per deviceCanonicalBest value per metric, deduplicated NormalizeDeduplicate + select

Raw is what the device or platform sends. Garmin wraps distance in a flat JSON field; Apple Health nests it inside a workout object with different keys. Sleep from an Oura Ring arrives as a deeply nested hierarchy with hypnogram samples and temperature deltas, while Fitbit sends a flat list of duration fields. You never see this layer.

Normalized is per-device, per-metric data converted to Sonar's unified schema — consistent field names, SI units, UTC timestamps. Each device's contribution is stored separately here.

Canonical is what you query. When a user has multiple devices reporting the same metric, Sonar selects the best value using priority rules. One row per metric per day per user.

Querying Data

The REST API always returns canonical data by default:

text
GET /v1/users/{user_id}/daily?metrics=steps,resting_heart_rate&start_date=2025-01-08&end_date=2025-01-14

To see which device contributed each value, add include_sources=true:

json
{
  "date": "2025-01-14",
  "metric": "resting_heart_rate",
  "value": 52,
  "unit": "bpm",
  "source": {
    "provider": "apple_health",
    "device_id": "dev_001",
    "original_value": 52,
    "original_unit": "bpm"
  }
}

Multi-Device Deduplication

Some users have more than one wearable — an Apple Watch for workouts and an Oura Ring for sleep, or a Garmin for running and a Whoop for recovery. When two devices report the same metric for the same time period, Sonar resolves it at the metric level, not the device level. Three rules govern this:

  1. Priority ranking — each device type has a default priority per metric category. Mobile SDK data ranks highest by default because it arrives directly from the device with minimal delay.
  2. Per-metric resolution — deduplication is metric-by-metric. A user's steps might come from their Apple Watch while their sleep comes from their Oura Ring.
  3. No data loss — all source data is stored. The canonical layer presents one clean value per metric per period, but every device's raw data remains accessible via include_sources=true.

Sonar ships with sensible defaults — for example, preferring ring data for sleep and wrist-worn devices for heart rate. You can override these priorities per metric type through the API to match your use case.

See API Conventions for general API patterns.

Available Metrics

Sonar normalizes data into metrics across seven categories — activity, sleep, vitals, body composition, energy, nutrition, and scores. The Data Catalog is the authoritative reference for every metric_id, its unit, and description.

The sections below describe each API endpoint and the shape of data it returns. Use the metric_id values from the Data Catalog in all API requests.

Daily Summaries

Aggregated per day. Query via GET /v1/users/{user_id}/daily. Returns one row per metric per day. Pass any combination of metric_id values from the Activity, Vitals, Body Composition, Energy, and Nutrition categories.

Sleep

Per-session data. Query via GET /v1/users/{user_id}/sleep.

Returns sleep sessions with duration, stages (light, deep, REM), efficiency, and overnight vitals (heart rate, HRV, respiratory rate, SpO2, temperature).

Timeseries

Intraday samples, useful for charts. Query via GET /v1/users/{user_id}/timeseries.

Supported metrics: heart_rate, heart_rate_variability, stress_score, blood_oxygen, steps, active_calories.

Resolution varies by device (1-second to 15-minute samples).

Workouts

Per-session workout data. Query via GET /v1/users/{user_id}/workouts.

Each session includes: sport type, start/end timestamps, duration, distance, active calories, average and max heart rate, heart rate zone breakdown, and elevation gain.

Units

All values use metric units by default:

Dimension Unit Example
Distance km distance, running, cycling
Swimming distance m swimming
Temperature °C sleep_body_temperature, sleep_skin_temperature
Weight kg weight, lean_body_mass
Heart rate bpm heart_rate, resting_heart_rate
HRV ms heart_rate_variability, heart_rate_variability_rmssd
Energy kcal active_calories, resting_energy
Duration min exercise, asleep, deep_sleep
Timestamps ISO 8601, UTC All date and time fields

Pass unit_system=imperial to receive miles, Fahrenheit, and pounds instead. See API Conventions for details.

Data Freshness

Data freshness depends on the connection type:

Connection Type Sync Frequency Typical Latency
Apple Health (SDK) Background delivery, configurable Seconds to minutes
Health Connect (SDK) Background delivery, configurable Seconds to minutes
Cloud wearables (Garmin, Oura, etc.) Provider push + periodic polling 5–15 minutes

Use last_sync_at on the user object to know when fresh data is available.

Common Questions

What happens when a user switches devices?

The canonical layer handles it automatically. If a user replaces their Garmin with an Apple Watch, new data comes from the new device and deduplication rules apply as usual. Historical data from the old device remains queryable unless the device is disconnected. Scores recalibrate based on the new data source within a few days.

Should I cache scores or query them fresh?

Scores update at most a few times per day (after syncs and consolidation). For dashboard-style UIs, querying on page load is fine. For high-traffic endpoints, cache scores for 5–15 minutes.

How do I query data for a large date range?

The /daily endpoint handles ranges up to 90 days in a single request. For longer ranges, paginate by splitting into 90-day windows. Timeseries data (intraday samples) should be queried in smaller windows — 7 days is a practical maximum per request to keep response sizes reasonable.

Can I get raw data instead of canonical?

Not through the REST API — Sonar intentionally exposes only the canonical layer to keep your integration simple. If you need to see which device contributed a specific value, use include_sources=true on the query. This adds a source object to each data point without changing the canonical value itself.