Skip to main content

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.

Response Format

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

GET /api/v1/users
Query Parameters:
ParameterTypeDescription
searchstringFilter by name or email
limitintegerMax results (default: 50)
offsetintegerSkip 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:
ParameterTypeDescription
user_idUUIDUser 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:
FieldTypeRequiredDescription
emailstringYesValid email address
first_namestringNoUser’s first name
last_namestringNoUser’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 Content
This 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:
ParameterTypeDescription
start_dateISO 8601Start of date range
end_dateISO 8601End of date range
sourcestringFilter 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:
ParameterTypeDescription
workout_typestringFilter by activity (running, cycling, etc.)
cursorstringPagination cursor for next page
limitintegerResults 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

All distances converted to meters:
{
  "distance_meters": 8000,
  "distance_km": 8.0,
  "distance_miles": 4.97
}

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.

Pagination

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:
CodeMeaningDescription
200OKRequest succeeded
201CreatedResource created successfully
204No ContentRequest succeeded, no body returned
400Bad RequestInvalid request parameters
401UnauthorizedMissing or invalid API key
404Not FoundResource doesn’t exist
429Too Many RequestsRate limit exceeded
500Internal Server ErrorServer 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"
)

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

Build docs developers (and LLMs) love