Receive real-time notifications when health data arrives for your users. Register HTTPS endpoints, filter by event type or user, verify signatures, and debug delivery.
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.
Store this secret securely on your server — you’ll use it to verify every incoming request.
3
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)
from svix.webhooks import Webhook, WebhookVerificationErrorfrom fastapi import FastAPI, Request, HTTPExceptionapp = FastAPI()WEBHOOK_SECRET = "whsec_C2FVsBQIhrscChlQIMV+b5ND9vAIBZaM7ZqtLnNYGDA="@app.post("/webhooks/health")async def handle_webhook(request: Request): headers = dict(request.headers) payload = await request.body() try: wh = Webhook(WEBHOOK_SECRET) event = wh.verify(payload, headers) except WebhookVerificationError: raise HTTPException(status_code=400, detail="Invalid signature") event_type = event["type"] if event_type == "workout.created": data = event["data"] print(f"New workout for user {data['user_id']}: " f"{data['type']} — {data['duration_seconds']}s") elif event_type == "sleep.created": data = event["data"] print(f"New sleep for user {data['user_id']}: " f"efficiency {data.get('efficiency_percent')}%") return {"ok": True}
import { Webhook } from "svix";import express from "express";const app = express();const WEBHOOK_SECRET = "whsec_C2FVsBQIhrscChlQIMV+b5ND9vAIBZaM7ZqtLnNYGDA=";app.post( "/webhooks/health", express.raw({ type: "application/json" }), (req, res) => { const wh = new Webhook(WEBHOOK_SECRET); let event; try { event = wh.verify(req.body, req.headers); } catch (err) { return res.status(400).json({ error: "Invalid signature" }); } if (event.type === "workout.created") { console.log(`Workout for user ${event.data.user_id}:`, event.data.type, event.data.duration_seconds + "s"); } res.json({ ok: true }); });
# The three signature headers sent with every request:# svix-id — unique message ID (for idempotency)# svix-timestamp — Unix timestamp of delivery attempt# svix-signature — comma-separated list of v1,<base64_hmac> signatures## To verify manually:# 1. Build the signed content: "{svix-id}.{svix-timestamp}.{raw_body}"# 2. HMAC-SHA256 the content with the base64-decoded key (strip "whsec_" prefix)# 3. Base64-encode the result# 4. Compare with the signature in the svix-signature header
Install the Svix library: pip install svix / npm install svix. The Webhook.verify() call handles timestamp tolerance (rejects messages older than 5 minutes) and all signature edge cases automatically.
4
Send a test event
Trigger a realistic example payload to your endpoint to confirm end-to-end delivery before you go live.
Fired per ingestion batch — one event per distinct (user, provider, series_type) combination in a sync run. These events signal that raw samples are available to query via GET /api/v1/users/{user_id}/timeseries.
start_time / end_time are null when the ingestion run did not record time bounds (e.g. Apple Health XML imports). Fetch the actual samples via GET /api/v1/users/{user_id}/timeseries.
from svix.webhooks import Webhook, WebhookVerificationErrorwh = Webhook("whsec_YOUR_SIGNING_SECRET")try: # headers must include svix-id, svix-timestamp, svix-signature payload = wh.verify(raw_body_bytes, headers_dict)except WebhookVerificationError: # reject — signature invalid or timestamp too old return 400
Install: pip install svix
import { Webhook } from "svix";const wh = new Webhook("whsec_YOUR_SIGNING_SECRET");try { const payload = wh.verify(rawBodyBuffer, headersObject);} catch (err) { // reject — signature invalid or timestamp too old return res.status(400).end();}
Install: npm install svix
# 1. Decode the key: base64_decode(key.removePrefix("whsec_"))# 2. Build the signed string: "{svix-id}.{svix-timestamp}.{raw_body}"# 3. Compute: HMAC-SHA256(key_bytes, signed_string)# 4. Encode: base64(hmac_bytes)# 5. Compare with each "v1,<value>" in the svix-signature header
Always verify signatures before processing the payload. This prevents replay attacks and ensures events cannot be forged by third parties.
The svix-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.
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
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
Respond with a 2xx status code as quickly as possible (before doing any heavy processing).
Offload slow work to a background queue — process the event asynchronously.
Return 2xx even for events you choose to ignore (otherwise they’ll be retried).
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.