Overview
The Unified API provides a consistent REST interface to access health data regardless of the source wearable device. Instead of implementing separate integrations for Garmin, Apple Health, Whoop, and other providers, you make standardized API calls and receive normalized data.
All API endpoints are documented interactively at http://localhost:8000/docs when running Open Wearables.
API Architecture
Base URL
All API requests use the base URL:
http://localhost:8000/api/v1
For production deployments, replace with your hosted domain.
Authentication
All requests require an API key in the Authorization header:
curl -H "Authorization: Bearer YOUR_API_KEY" \
http://localhost:8000/api/v1/users
API keys provide full access to your data. Keep them secure and never commit them to version control.
All endpoints return JSON responses with consistent structure:
{
"id" : "550e8400-e29b-41d4-a716-446655440000" ,
"email" : "[email protected] " ,
"first_name" : "John" ,
"last_name" : "Doe" ,
"created_at" : "2024-03-07T12:00:00Z"
}
Error responses include detailed messages:
{
"detail" : "User not found"
}
Core Endpoints
User Management
Query Parameters: Parameter Type Description searchstring Filter by name or email limitinteger Max results (default: 50) offsetinteger Skip N results
Response: [
{
"id" : "550e8400-e29b-41d4-a716-446655440000" ,
"email" : "[email protected] " ,
"first_name" : "John" ,
"last_name" : "Doe" ,
"created_at" : "2024-03-07T12:00:00Z"
}
]
GET /api/v1/users/{user_id}
Path Parameters: Parameter Type Description user_idUUID User identifier
Response: {
"id" : "550e8400-e29b-41d4-a716-446655440000" ,
"email" : "[email protected] " ,
"first_name" : "John" ,
"last_name" : "Doe" ,
"created_at" : "2024-03-07T12:00:00Z" ,
"updated_at" : "2024-03-07T15:30:00Z"
}
POST /api/v1/users
Content-Type: application/json
{
"email" : "[email protected] ",
"first_name" : "Jane",
"last_name" : "Smith"
}
Request Body: Field Type Required Description emailstring Yes Valid email address first_namestring No User’s first name last_namestring No User’s last name
Response: 201 Created{
"id" : "660e8400-e29b-41d4-a716-446655440000" ,
"email" : "[email protected] " ,
"first_name" : "Jane" ,
"last_name" : "Smith" ,
"created_at" : "2024-03-07T16:45:00Z"
}
PATCH /api/v1/users/{user_id}
Content-Type: application/json
{
"first_name" : "Jane Updated"
}
Partial Updates Supported : Only include fields you want to change.Response: 200 OK
DELETE /api/v1/users/{user_id}
Response: 204 No ContentThis permanently deletes the user and all associated health data. This action cannot be undone.
Health Data Access
GET /api/v1/users/{user_id}/health/heart-rate?start_date=2024-03-01 & end_date = 2024-03-07
Query Parameters: Parameter Type Description start_dateISO 8601 Start of date range end_dateISO 8601 End of date range sourcestring Filter by provider (garmin, polar, etc.)
Response: [
{
"timestamp" : "2024-03-07T08:00:00Z" ,
"value" : 72 ,
"unit" : "bpm" ,
"source" : "garmin" ,
"confidence" : 0.95
}
]
GET /api/v1/users/{user_id}/health/steps?start_date=2024-03-01 & end_date = 2024-03-07
Returns daily step counts aggregated from all sources. Response: [
{
"date" : "2024-03-07" ,
"steps" : 10500 ,
"source" : "apple" ,
"goal" : 10000 ,
"goal_met" : true
}
]
GET /api/v1/users/{user_id}/health/sleep?start_date=2024-03-01 & end_date = 2024-03-07
Response: [
{
"id" : "770e8400-e29b-41d4-a716-446655440000" ,
"start_time" : "2024-03-06T22:30:00Z" ,
"end_time" : "2024-03-07T06:45:00Z" ,
"duration_minutes" : 495 ,
"stages" : {
"deep" : 120 ,
"light" : 240 ,
"rem" : 90 ,
"awake" : 45
},
"quality_score" : 85 ,
"source" : "whoop"
}
]
GET /api/v1/users/{user_id}/workouts?start_date=2024-03-01 & end_date = 2024-03-07
Query Parameters: Parameter Type Description workout_typestring Filter by activity (running, cycling, etc.) cursorstring Pagination cursor for next page limitinteger Results per page (default: 20)
Response: {
"items" : [
{
"id" : "880e8400-e29b-41d4-a716-446655440000" ,
"user_id" : "550e8400-e29b-41d4-a716-446655440000" ,
"workout_type" : "running" ,
"start_time" : "2024-03-07T07:00:00Z" ,
"duration_minutes" : 45 ,
"distance_meters" : 8000 ,
"calories" : 450 ,
"avg_heart_rate" : 155 ,
"max_heart_rate" : 178 ,
"source" : "strava"
}
],
"next_cursor" : "eyJpZCI6Ijg4MGU4NDAwIn0" ,
"has_more" : true
}
GET /api/v1/users/{user_id}/health/body?start_date=2024-03-01 & end_date = 2024-03-07
Response: [
{
"date" : "2024-03-07" ,
"weight_kg" : 75.5 ,
"body_fat_percentage" : 18.5 ,
"bmi" : 23.2 ,
"source" : "garmin"
}
]
Provider Connections
GET /api/v1/users/{user_id}/connections
Lists all provider connections for a user. Response: [
{
"id" : "990e8400-e29b-41d4-a716-446655440000" ,
"user_id" : "550e8400-e29b-41d4-a716-446655440000" ,
"provider" : "garmin" ,
"status" : "active" ,
"last_synced_at" : "2024-03-07T12:00:00Z" ,
"created_at" : "2024-02-01T10:00:00Z"
}
]
POST /api/v1/users/{user_id}/connections/{provider}/sync
Manually triggers data synchronization for a specific provider. Response: 202 Accepted{
"task_id" : "task-abc123" ,
"status" : "queued" ,
"message" : "Sync task queued successfully"
}
DELETE /api/v1/users/{user_id}/connections/{provider}
Revokes OAuth tokens and disconnects the provider. Response: 204 No Content
Data Normalization
Open Wearables normalizes data from different providers into consistent formats:
Units Standardization
Distance
Weight
Heart Rate
Timestamps
All distances converted to meters: {
"distance_meters" : 8000 ,
"distance_km" : 8.0 ,
"distance_miles" : 4.97
}
All weights in kilograms: {
"weight_kg" : 75.5 ,
"weight_lbs" : 166.4
}
All heart rates in beats per minute: {
"value" : 72 ,
"unit" : "bpm"
}
All timestamps in ISO 8601 UTC: {
"timestamp" : "2024-03-07T12:00:00Z"
}
Workout Type Mapping
Common workout types are standardized across providers:
[
"running" ,
"cycling" ,
"swimming" ,
"walking" ,
"yoga" ,
"strength_training" ,
"cardio" ,
"other"
]
Provider-specific types are mapped to standard categories in backend/app/mappings.py.
Rate Limiting
API rate limits protect the service from abuse:
Per API Key : 1000 requests per hour
Per IP : 100 requests per minute (unauthenticated)
Rate limit headers included in all responses:
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 987
X-RateLimit-Reset: 1678195200
Contact support if you need higher rate limits for your use case.
Large result sets use cursor-based pagination:
# First page
GET /api/v1/users/USER_ID/workouts?limit= 20
# Response includes next_cursor
{
"items" : [...],
"next_cursor" : "eyJpZCI6Ijg4MGU4NDAwIn0",
"has_more" : true
}
# Next page
GET /api/v1/users/USER_ID/workouts?limit= 20 & cursor = eyJpZCI6Ijg4MGU4NDAwIn0
Error Handling
The API returns standard HTTP status codes:
Code Meaning Description 200OK Request succeeded 201Created Resource created successfully 204No Content Request succeeded, no body returned 400Bad Request Invalid request parameters 401Unauthorized Missing or invalid API key 404Not Found Resource doesn’t exist 429Too Many Requests Rate limit exceeded 500Internal Server Error Server error (contact support)
Error Response Format:
{
"detail" : "User not found" ,
"status_code" : 404 ,
"error_code" : "USER_NOT_FOUND"
}
SDK Examples
import httpx
class OpenWearablesClient :
def __init__ ( self , api_key : str , base_url : str = "http://localhost:8000" ):
self .api_key = api_key
self .base_url = base_url
self .client = httpx.Client(
headers = { "Authorization" : f "Bearer { api_key } " },
base_url = base_url,
timeout = 30.0
)
def get_users ( self , search : str | None = None ):
params = { "search" : search} if search else {}
response = self .client.get( "/api/v1/users" , params = params)
response.raise_for_status()
return response.json()
def get_heart_rate ( self , user_id : str , start_date : str , end_date : str ):
response = self .client.get(
f "/api/v1/users/ { user_id } /health/heart-rate" ,
params = { "start_date" : start_date, "end_date" : end_date}
)
response.raise_for_status()
return response.json()
# Usage
client = OpenWearablesClient( api_key = "your-api-key" )
users = client.get_users()
heart_rate = client.get_heart_rate(
user_id = "550e8400-e29b-41d4-a716-446655440000" ,
start_date = "2024-03-01" ,
end_date = "2024-03-07"
)
interface User {
id : string ;
email : string ;
first_name ?: string ;
last_name ?: string ;
created_at : string ;
}
class OpenWearablesClient {
private apiKey : string ;
private baseUrl : string ;
constructor ( apiKey : string , baseUrl : string = "http://localhost:8000" ) {
this . apiKey = apiKey ;
this . baseUrl = baseUrl ;
}
private async request < T >( endpoint : string , options ?: RequestInit ) : Promise < T > {
const response = await fetch ( ` ${ this . baseUrl }${ endpoint } ` , {
... options ,
headers: {
"Authorization" : `Bearer ${ this . apiKey } ` ,
"Content-Type" : "application/json" ,
... options ?. headers ,
},
});
if ( ! response . ok ) {
throw new Error ( `API error: ${ response . statusText } ` );
}
return response . json ();
}
async getUsers ( search ?: string ) : Promise < User []> {
const params = new URLSearchParams ();
if ( search ) params . append ( "search" , search );
return this . request < User []>( `/api/v1/users? ${ params } ` );
}
async getHeartRate ( userId : string , startDate : string , endDate : string ) {
const params = new URLSearchParams ({
start_date: startDate ,
end_date: endDate ,
});
return this . request (
`/api/v1/users/ ${ userId } /health/heart-rate? ${ params } `
);
}
}
// Usage
const client = new OpenWearablesClient ( "your-api-key" );
const users = await client . getUsers ();
const heartRate = await client . getHeartRate (
"550e8400-e29b-41d4-a716-446655440000" ,
"2024-03-01" ,
"2024-03-07"
);
WebSocket Support (Coming Soon)
Real-time data streaming via WebSockets:
const ws = new WebSocket ( 'ws://localhost:8000/api/v1/stream' );
ws . onmessage = ( event ) => {
const data = JSON . parse ( event . data );
console . log ( 'New health data:' , data );
};
Next Steps
OAuth Flow Connect users to their wearable providers
Provider Support Learn which devices and platforms are supported