Overview
Outgoing webhooks let Open Wearables push events to your backend in real time, so you don’t need to poll for new data. Each time a workout is saved, sleep is recorded, or a timeseries batch is ingested, Open Wearables fires an HTTP POST request to every registered endpoint that matches the event. Webhooks are delivered via Svix, which handles retries, signature signing, and delivery history.Things you can do
Trigger downstream processing when a workout is synced, update your UI in real time when sleep data arrives, scope an endpoint to a single user’s events, or verify payloads are genuinely from Open Wearables.
Requirements
A developer account and a Bearer token (from
POST /api/v1/auth/login), plus a publicly reachable HTTPS URL for your endpoint.Quickstart
Register an endpoint
Send the URL your server is listening on. This returns an endpoint object you’ll use in subsequent calls.Response:Save the
id — you’ll need it to fetch the signing secret and inspect delivery attempts.Get the signing secret
Retrieve the HMAC signing key for your endpoint to verify incoming payloads.Response:Store this secret securely on your server — you’ll use it to verify every incoming request.
Handle incoming events
Open Wearables sends a
POST to your URL with a JSON body and three signature headers. Verify the signature before processing.- Python
- Node.js
- cURL (manual verify)
Event Types
All events follow theresource.action naming convention. Use GET /api/v1/webhooks/event-types to retrieve this list programmatically.
Session Events
Fired once when a complete session is saved or merged.| Event | Description |
|---|---|
connection.created | A user successfully connected a wearable provider. |
connection.revoked | A connection became invalid (refresh token expired/revoked, or the user deregistered on the provider side). The user must re-authorize to resume syncing. |
workout.created | A new workout session was saved. |
sleep.created | A new (or merged) sleep session was saved. |
menstrual_cycle.created | A new menstrual cycle record was saved. |
activity.created | A new generic activity session was saved. |
Timeseries Events
Fired per ingestion batch — one event per distinct(user, provider, series_type) combination in a sync run. Each event carries the full samples array, so consumers can store data directly from the webhook without issuing follow-up API calls.
| Event | Series types included |
|---|---|
heart_rate.created | heart_rate, resting_heart_rate, walking_heart_rate_average, atrial_fibrillation_burden |
heart_rate_variability.created | heart_rate_variability_sdnn, heart_rate_variability_rmssd |
steps.created | steps |
calories.created | energy, basal_energy |
spo2.created | oxygen_saturation, peripheral_perfusion_index |
respiratory_rate.created | respiratory_rate, sleeping_breathing_disturbances |
body_temperature.created | body_temperature, skin_temperature, skin_temperature_deviation |
stress.created | garmin_stress_level, electrodermal_activity |
blood_glucose.created | blood_glucose, blood_alcohol_content, insulin_delivery |
blood_pressure.created | blood_pressure_systolic, blood_pressure_diastolic |
body_composition.created | weight, body_fat_percentage, body_mass_index, lean_body_mass, … |
fitness_metrics.created | vo2_max, cardiovascular_age, garmin_fitness_age |
recovery_score.created | recovery_score, garmin_body_battery |
activity_timeseries.created | stand_time, exercise_time, flights_climbed, distance types, … |
workout_metrics.created | cadence, power, speed, running/walking/swimming metrics |
environmental.created | environmental_audio_exposure, uv_exposure, weather_temperature, … |
timeseries.created | Catch-all for series types not yet explicitly mapped. |
Payload Reference
Every payload envelope hastype (the event name as a string) and data.
connection.created
connection.revoked
reason is a short cause, e.g. refresh_failed or deregistration.
workout.created
Fields like
calories_kcal, distance_meters, and heart-rate fields are null when the provider did not report them.sleep.created
menstrual_cycle.created
pregnancy_snapshot is populated (as a list with one entry) when the user is tracking a pregnancy. Garmin reuses the same summary ID for daily cycle updates — Open Wearables deduplicates by external_id, so the event fires only when a new record is first inserted.activity.created
Timeseries events (all share the same shape)
Each timeseries event carries the full sample array so your backend can act on the data immediately — no follow-up API call needed.samples matches the schema returned by GET /api/v1/users/{user_id}/timeseries, so consumers work with a single schema regardless of whether data comes from a webhook or the pull API.
is_daily_total distinguishes pre-aggregated daily totals from intraday samples for additive series (steps, energy, distance, flights):| Value | Meaning |
|---|---|
true | Pre-aggregated daily total reported by the device. Use this value directly — do not add intraday samples from the same day and source on top of it. |
false | Intraday sample (e.g. a 15-minute epoch). Sum these when no daily total is present. |
null | Legacy row ingested before this field was introduced, or a series type where daily vs. intraday does not apply (e.g. heart rate). Treat the same as false. |
is_daily_total is always null and can be ignored.Large batches (exceeding 2 500 samples) are automatically split into consecutive chunk events to stay within Svix’s 1 MB payload limit. Each chunk event includes
chunk_index (0-based) and total_chunks so you can detect and reassemble split deliveries. The sample_count field always reflects the total number of samples across all chunks.GET /api/v1/users/{user_id}/timeseries) remains available for backfill, reconciliation, or recovery after missed deliveries.
Filtering Events
Filter by event type
Passfilter_types when creating or updating an endpoint to receive only the events you care about.
Filter by user
Passuser_id to scope an endpoint to events for a single user. All other users’ events are silently dropped before delivery.
PATCH with "user_id": null:
Signature Verification
Every delivery includes three headers that let you confirm the payload was sent by Open Wearables and hasn’t been tampered with:| Header | Description |
|---|---|
svix-id | Unique message ID. Use this for idempotency — the same ID is retried on failure. |
svix-timestamp | Unix seconds timestamp of the original send. Requests older than 5 minutes are auto-rejected by the Svix SDK. |
svix-signature | Comma-separated list of v1,<base64_hmac> values. |
Using the Svix SDK (recommended)
- Python
- Node.js
- Manual (any language)
pip install svixIdempotency
Thesvix-id header is stable across retries — the same logical event always carries the same ID. Store received IDs and skip duplicates to make your handler idempotent.
Managing Endpoints
List all endpoints
Update an endpoint
All fields are optional — send only what you want to change.Delete an endpoint
204 No Content on success.
Delivery and Retries
Open Wearables (via Svix) retries failed deliveries automatically with exponential back-off. A delivery is considered failed when your endpoint returns a non-2xx status code or does not respond within the timeout.Retry schedule
Retry schedule
Svix retries each failed message at increasing intervals. If all retries are exhausted the message is marked as failed in delivery history — you can inspect it and manually trigger a resend from the Svix dashboard.
Expected endpoint behaviour
Expected endpoint behaviour
- Respond with a
2xxstatus code as quickly as possible (before doing any heavy processing). - Offload slow work to a background queue — process the event asynchronously.
- Return
2xxeven for events you choose to ignore (otherwise they’ll be retried).
Webhook availability
Webhook availability
Data ingestion is never blocked by webhook failures — if delivery infrastructure is temporarily unavailable, data continues to be stored and events are queued for retry.
Debugging
View delivery history for an endpoint
View all sent messages
Send a test event
Send a realistic example payload for any event type to an endpoint without waiting for real data:workout.created.
