Skip to main content

Documentation Index

Fetch the complete documentation index at: https://openwearables.io/docs/llms.txt

Use this file to discover all available pages before exploring further.

Overview

The sync status API lets you observe the entire lifecycle of every sync that runs for a user — across pulls, webhooks, SDK uploads, Apple Health XML imports, and historical Garmin backfills — in real time. Three endpoints are exposed:
  • GET /api/v1/users/{user_id}/sync/stream — long-lived Server-Sent Events connection that streams every event as it happens.
  • GET /api/v1/users/{user_id}/sync/recent — the last N events buffered in Redis (default 50, max 200, retained for 24 hours).
  • GET /api/v1/users/{user_id}/sync/runs — the latest event per run (one row per run_id), so you can quickly inspect which syncs are in progress and how recent ones ended.
All three accept the same ApiKeyDep authentication as the rest of the public API: pass your API key via the X-API-Key header.
curl -H "X-API-Key: YOUR_API_KEY" \
  "https://api.openwearables.io/api/v1/users/USER_ID/sync/stream"
HTTP status codes (all three endpoints)
CodeMeaning
200Success
401Missing or invalid API key
404User not found
422Validation error (invalid parameter value)
500Internal server error
Error response shape
{ "detail": "User not found" }
The SSE endpoint also supports webhooks — if you have outgoing webhook endpoints registered, terminal sync events (sync.completed, sync.failed, sync.cancelled) are also dispatched as Svix webhooks. See the Webhooks guide.

Event Schema

Every event is a JSON object emitted as the data field of an SSE message with event: sync.status:
{
  "event_id": "evt_01HZ...",
  "run_id": "pull_garmin_user42_1730000000",
  "user_id": "user42",
  "provider": "garmin",
  "source": "pull",
  "stage": "fetching",
  "status": "in_progress",
  "message": "Fetching workouts",
  "progress": 0.42,
  "items_processed": 21,
  "items_total": 50,
  "error": null,
  "metadata": {},
  "started_at": "2024-10-27T10:15:00Z",
  "ended_at": null,
  "timestamp": "2024-10-27T10:15:32Z"
}

Fields

FieldTypeDescription
run_idstringUnique identifier per sync run. Multiple events share a run_id as a sync progresses.
providerstringLower-case provider slug (e.g. garmin, oura, apple).
sourceSyncSourceOne of pull, webhook, sdk, backfill, xml_import.
stageSyncStageOne of queued, started, fetching, processing, saving, completed, failed, cancelled.
statusSyncStatusOne of in_progress, success, partial, failed, cancelled. Terminal statuses end a run.
progressnumber | nullOptional 0..1 fraction. Not all syncs report this.
items_processednumber | nullOptional count of items processed so far.
items_totalnumber | nullOptional total when known.
started_atstring | nullISO-8601 timestamp when the run started.
ended_atstring | nullISO-8601 timestamp when the run reached a terminal status.
timestampstringISO-8601 timestamp of this individual event.

Streaming endpoint

curl -N "https://api.openwearables.io/api/v1/users/USER_ID/sync/stream?replay=20" \
  -H "X-API-Key: YOUR_API_KEY"
Path parameters
ParameterTypeRequiredDescription
user_idstring (UUID)RequiredThe user whose sync events to stream.
Query parameters
ParameterTypeDefaultRangeDescription
replayinteger201..200Number of recent events to replay before the live stream begins. Allows a freshly connected client to catch up on in-progress syncs.
The replay parameter causes the most recent events from the last 24 hours to be replayed before the live stream begins, so a freshly connected client can see in-progress syncs immediately. The stream emits:
  • A connect comment (: connected) when the connection is established.
  • One SSE message per event (event: sync.status, data: <json>).
  • A : heartbeat comment every 15 seconds to keep the connection alive.
Browsers can consume the stream using EventSource (without auth headers) or fetch + ReadableStream (with a Bearer JWT token in the Authorization header). Both authentication methods are accepted by the same endpoint — no separate dashboard variant is needed. Example SSE stream output
: connected

event: sync.status
data: {"event_id":"evt_01HZ...","run_id":"pull_garmin_user42_1730000000","provider":"garmin","stage":"fetching","status":"in_progress","progress":0.42,...}

: heartbeat

event: sync.status
data: {"event_id":"evt_01HZ...","run_id":"pull_garmin_user42_1730000000","provider":"garmin","stage":"completed","status":"success","progress":1.0,"ended_at":"2024-10-27T10:16:45Z",...}
Error responses
HTTP/1.1 401 Unauthorized
{ "detail": "Invalid authentication credentials" }
HTTP/1.1 404 Not Found
{ "detail": "User not found" }

Example: Node.js

import { EventSource } from 'eventsource';

const url =
  'https://api.openwearables.io/api/v1/users/USER_ID/sync/stream?replay=20';
const es = new EventSource(url, {
  fetch: (input, init) =>
    fetch(input, { ...init, headers: { ...init.headers, 'X-API-Key': KEY } }),
});

es.addEventListener('sync.status', (msg) => {
  const event = JSON.parse(msg.data);
  console.log(`[${event.provider}] ${event.stage} - ${event.status}`);
});

Recent events

curl "https://api.openwearables.io/api/v1/users/USER_ID/sync/recent?limit=50" \
  -H "X-API-Key: YOUR_API_KEY"
Path parameters
ParameterTypeRequiredDescription
user_idstring (UUID)RequiredThe user whose events to return.
Query parameters
ParameterTypeDefaultRangeDescription
limitinteger501..200Maximum number of events to return, ordered newest first.
Events are retained in Redis for 24 hours. Useful when reconnecting or rendering a “history” tab to seed the UI before opening the SSE stream. Success response — array of SyncStatusEvent objects, newest first.
[
  {
    "event_id": "evt_01HZ...",
    "run_id": "pull_garmin_user42_1730000000",
    "user_id": "user42",
    "provider": "garmin",
    "source": "pull",
    "stage": "completed",
    "status": "success",
    "message": null,
    "progress": 1.0,
    "items_processed": 42,
    "items_total": 42,
    "error": null,
    "metadata": {},
    "started_at": "2024-10-27T10:15:00Z",
    "ended_at": "2024-10-27T10:16:45Z",
    "timestamp": "2024-10-27T10:16:45Z"
  }
]

Sync run summaries

curl "https://api.openwearables.io/api/v1/users/USER_ID/sync/runs?limit=20" \
  -H "X-API-Key: YOUR_API_KEY"
Path parameters
ParameterTypeRequiredDescription
user_idstring (UUID)RequiredThe user whose run summaries to return.
Query parameters
ParameterTypeDefaultRangeDescription
limitinteger201..50Maximum number of run summaries to return.
Returns the latest event for each unique run_id from the past 24 hours (one row per run), ordered newest first. Each run_id is stable for the entire lifetime of a single sync invocation — multiple progress events share the same run_id and only the most recent one is surfaced here. For Garmin backfill, the run_id is unique per execution and incorporates an execution-scoped trace identifier. Success response — array of run summary objects:
[
  {
    "run_id": "pull_garmin_user42_1730000000",
    "user_id": "user42",
    "provider": "garmin",
    "source": "pull",
    "stage": "completed",
    "status": "success",
    "message": null,
    "progress": 1.0,
    "items_processed": 42,
    "items_total": 42,
    "error": null,
    "started_at": "2024-10-27T10:15:00Z",
    "ended_at": "2024-10-27T10:16:45Z",
    "last_update": "2024-10-27T10:16:45Z"
  },
  {
    "run_id": "garmin_backfill_user42_trace-abc123",
    "user_id": "user42",
    "provider": "garmin",
    "source": "backfill",
    "stage": "fetching",
    "status": "in_progress",
    "message": "Fetching activities window 3 of 12",
    "progress": 0.25,
    "items_processed": null,
    "items_total": null,
    "error": null,
    "started_at": "2024-10-27T09:00:00Z",
    "ended_at": null,
    "last_update": "2024-10-27T09:45:00Z"
  }
]

Rate limiting

The sync status endpoints share the global API rate limit. The SSE stream endpoint (/sync/stream) holds a single long-lived connection per client — there is no per-user connection cap, but you should maintain at most one open stream per user and reuse it rather than opening a new one on each page load. For polling-based fallbacks using /sync/recent or /sync/runs, a reasonable poll interval is 5–15 seconds. Polling faster than once per second provides no meaningful benefit since event latency is already sub-second via the SSE stream. Retry guidance If the SSE stream disconnects (network error, server restart, or a 5xx response), implement exponential back-off before reconnecting:
  1. On first disconnect, reconnect after 1 second.
  2. Double the interval on each subsequent failure (2 s, 4 s, 8 s…) up to a maximum of 30 seconds.
  3. Pass replay=<N> on reconnect so you don’t miss events that arrived while offline.

Retention

All sync status data is held in Redis with a 24-hour TTL. For long-term audit, subscribe to the corresponding sync.started / sync.completed / sync.failed outgoing webhooks, which are persisted by Svix.