> ## 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.

# Live Sync Status (SSE)

> Stream real-time sync lifecycle events for a user via Server-Sent Events. Track when pulls, webhooks, SDK uploads, Apple Health imports, and Garmin backfills start, progress, and complete.

## 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](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_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.

<CodeGroup>
  ```bash X-API-Key theme={null}
  curl -H "X-API-Key: YOUR_API_KEY" \
    "https://api.openwearables.io/api/v1/users/USER_ID/sync/stream"
  ```
</CodeGroup>

**HTTP status codes (all three endpoints)**

| Code  | Meaning                                    |
| ----- | ------------------------------------------ |
| `200` | Success                                    |
| `401` | Missing or invalid API key                 |
| `404` | User not found                             |
| `422` | Validation error (invalid parameter value) |
| `500` | Internal server error                      |

**Error response shape**

```json theme={null}
{ "detail": "User not found" }
```

<Note>
  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](/docs/api-reference/guides/webhooks).
</Note>

***

## Event Schema

Every event is a JSON object emitted as the `data` field of an SSE message with `event: sync.status`:

```json theme={null}
{
  "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

| Field             | Type             | Description                                                                                         |
| ----------------- | ---------------- | --------------------------------------------------------------------------------------------------- |
| `run_id`          | `string`         | Unique identifier per sync run. Multiple events share a `run_id` as a sync progresses.              |
| `provider`        | `string`         | Lower-case provider slug (e.g. `garmin`, `oura`, `apple`).                                          |
| `source`          | `SyncSource`     | One of `pull`, `webhook`, `sdk`, `backfill`, `xml_import`.                                          |
| `stage`           | `SyncStage`      | One of `queued`, `started`, `fetching`, `processing`, `saving`, `completed`, `failed`, `cancelled`. |
| `status`          | `SyncStatus`     | One of `in_progress`, `success`, `partial`, `failed`, `cancelled`. Terminal statuses end a run.     |
| `progress`        | `number \| null` | Optional 0..1 fraction. Not all syncs report this.                                                  |
| `items_processed` | `number \| null` | Optional count of items processed so far.                                                           |
| `items_total`     | `number \| null` | Optional total when known.                                                                          |
| `started_at`      | `string \| null` | ISO-8601 timestamp when the run started.                                                            |
| `ended_at`        | `string \| null` | ISO-8601 timestamp when the run reached a terminal status.                                          |
| `timestamp`       | `string`         | ISO-8601 timestamp of this individual event.                                                        |

***

## Streaming endpoint

```bash theme={null}
curl -N "https://api.openwearables.io/api/v1/users/USER_ID/sync/stream?replay=20" \
  -H "X-API-Key: YOUR_API_KEY"
```

**Path parameters**

| Parameter | Type            | Required | Description                           |
| --------- | --------------- | -------- | ------------------------------------- |
| `user_id` | `string (UUID)` | Required | The user whose sync events to stream. |

**Query parameters**

| Parameter | Type      | Default | Range    | Description                                                                                                                          |
| --------- | --------- | ------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `replay`  | `integer` | `20`    | `1..200` | Number 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**

```json theme={null}
HTTP/1.1 401 Unauthorized
{ "detail": "Invalid authentication credentials" }
```

```json theme={null}
HTTP/1.1 404 Not Found
{ "detail": "User not found" }
```

### Example: Node.js

```javascript theme={null}
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

```bash theme={null}
curl "https://api.openwearables.io/api/v1/users/USER_ID/sync/recent?limit=50" \
  -H "X-API-Key: YOUR_API_KEY"
```

**Path parameters**

| Parameter | Type            | Required | Description                      |
| --------- | --------------- | -------- | -------------------------------- |
| `user_id` | `string (UUID)` | Required | The user whose events to return. |

**Query parameters**

| Parameter | Type      | Default | Range    | Description                                               |
| --------- | --------- | ------- | -------- | --------------------------------------------------------- |
| `limit`   | `integer` | `50`    | `1..200` | Maximum 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`](#event-schema) objects, newest first.

```json theme={null}
[
  {
    "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

```bash theme={null}
curl "https://api.openwearables.io/api/v1/users/USER_ID/sync/runs?limit=20" \
  -H "X-API-Key: YOUR_API_KEY"
```

**Path parameters**

| Parameter | Type            | Required | Description                             |
| --------- | --------------- | -------- | --------------------------------------- |
| `user_id` | `string (UUID)` | Required | The user whose run summaries to return. |

**Query parameters**

| Parameter | Type      | Default | Range   | Description                                |
| --------- | --------- | ------- | ------- | ------------------------------------------ |
| `limit`   | `integer` | `20`    | `1..50` | Maximum 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:

```json theme={null}
[
  {
    "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`](/docs/api-reference/guides/webhooks) outgoing webhooks, which are persisted by Svix.
