Add a Wearable Provider Integration | Open Wearables
Add a new fitness data provider to Open Wearables. Covers Strategy, Factory, and Template Method patterns for OAuth, workout, 24/7 data, and webhook handlers. Consistent across all providers.
This guide walks you through the process of adding a new fitness data provider (e.g. Strava, Samsung Health, Xiaomi, WHOOP) to the OpenWearables platform. The architecture uses design patterns like Strategy, Factory, and Template Method to make adding new providers straightforward and consistent.
Workouts Handler - Fetches and normalizes workout/activity data
247 Data Handler - Fetches continuous health metrics (sleep, recovery, HR, etc.)
Webhook Handler - Receives and processes incoming push events from the provider
Each provider declares its data delivery modes via ProviderCapabilities:
Capability
Description
Example providers
supports_pull
REST API polling (OAuth)
Garmin, Oura, Whoop, Strava
supports_push
Incoming webhooks
Garmin, Oura, Strava, Suunto, Polar
supports_async_export
Provider initiates async export delivered via webhook
Garmin
supports_sdk
Data from mobile SDK client
Apple, Samsung, Google
supports_xml_import
File-based data import
Apple Health
webhook_notify_only
Webhook is a lightweight notification; actual data must be fetched via REST
Oura, Strava, Fitbit
For reference: Garmin sends the full data payload inside every webhook (webhook_notify_only=False). Oura and Strava send only a notification and you must fetch actual data via REST (webhook_notify_only=True).
Before starting, gather the following information about your provider:
Provider API Information
Base API URL (e.g. https://cloudapi.suunto.com)
Authentication method (usually OAuth 2.0)
Available data endpoints (activities, workouts, health metrics)
Rate limits and pagination
OAuth Configuration (if applicable)
Authorization URL
Token exchange URL
Required scopes
PKCE required (yes/no)
Credentials being sent via Authorization header or request body
Client credentials (ID and Secret)
Where redirect URL should be registered
Data Format
Workout/activity data structure
Timestamp format (Unix, ISO 8601, etc.)
Available metrics (heart rate, distance, calories, etc.)
Workout type mappings
Please, remember to provide svg icon for a new provider. It should be named <lowercase_provider_name>.svg and be placed in /backend/app/static/provider-icons.
The strategy class is the entry point for your provider. It defines the provider’s identity, declares its capabilities, and initializes its components.Create backend/app/services/providers/suunto/strategy.py:
from app.services.providers.base_strategy import BaseProviderStrategy, ProviderCapabilitiesfrom app.services.providers.suunto.oauth import SuuntoOAuthfrom app.services.providers.suunto.workouts import SuuntoWorkoutsclass SuuntoStrategy(BaseProviderStrategy): """Suunto provider implementation.""" def __init__(self): super().__init__() # Initialize OAuth component (skip for SDK/XML-only providers) self.oauth = SuuntoOAuth( user_repo=self.user_repo, connection_repo=self.connection_repo, provider_name=self.name, api_base_url=self.api_base_url, ) # Initialize workouts component self.workouts = SuuntoWorkouts( workout_repo=self.workout_repo, connection_repo=self.connection_repo, provider_name=self.name, api_base_url=self.api_base_url, oauth=self.oauth, ) # Webhook handler — only set if supports_push=True (see below) # self.webhooks = SuuntoWebhookHandler(...) @property def name(self) -> str: """Unique identifier for the provider (lowercase).""" return "suunto" @property def api_base_url(self) -> str: """Base URL for the provider's API.""" return "https://cloudapi.suunto.com" @property def capabilities(self) -> ProviderCapabilities: """Declare what data delivery modes this provider supports.""" return ProviderCapabilities( supports_pull=True, supports_push=True, webhook_notify_only=True, # Suunto sends notifications, not full payloads )
name: Must be unique and lowercase (used in URLs and database)
api_base_url: Used by the API client to construct requests
display_name: Optional, shown in UI (defaults to name.capitalize())
capabilities: Required — tells the unified router and sync scheduler how this provider delivers data
Set self.webhooks = None (default) if your provider has no incoming webhooks
Set self.oauth = None for SDK/file-upload-only providers like Apple Health
Inherited BaseProviderStrategy will init all required repositories so you don’t need to take care about database manipulations. You can read more about repositories role in our System Overview.
Create a mapping file to convert provider-specific workout types to unified types.Create backend/app/constants/workout_types/suunto.py:
"""Suunto activity type to OpenWearables unified workout type mapping."""from app.schemas.workout_types import WorkoutTypeSUUNTO_WORKOUT_TYPE_MAPPINGS: list[tuple[int, str, WorkoutType]] = [ (0, "Walking", WorkoutType.WALKING), (1, "Running", WorkoutType.RUNNING), (2, "Cycling", WorkoutType.CYCLING), # [...]}SUUNTO_ID_TO_UNIFIED: dict[int, WorkoutType] = { activity_id: unified_type for activity_id, _, unified_type in SUUNTO_WORKOUT_TYPE_MAPPINGS}SUUNTO_ID_TO_NAME: dict[int, str] = {activity_id: name for activity_id, name, _ in SUUNTO_WORKOUT_TYPE_MAPPINGS}def get_unified_workout_type(suunto_activity_id: int) -> WorkoutType: return SUUNTO_ID_TO_UNIFIED.get(suunto_activity_id, WorkoutType.OTHER)def get_activity_name(suunto_activity_id: int) -> str: """Get the Suunto activity name for a given ID.""" return SUUNTO_ID_TO_NAME.get(suunto_activity_id, "Unknown")
Review the existing unified workout types in your system before mapping. You may need to add new unified types to accommodate provider-specific activities.
If your provider delivers data via incoming webhooks, implement a BaseWebhookHandler subclass and wire it into your strategy.The unified router at POST /api/v1/providers/{provider}/webhooks automatically delegates all requests to strategy.webhooks — no new routes needed.
First decide how your provider delivers webhook data:
Full payload (push)
Provider sends the complete data in the webhook body. dispatch() saves records directly.
Example: Garmin
Notify-only
Provider sends a lightweight notification with user ID + event type. You must fetch actual data via REST inside dispatch().
Example: Oura, Strava, Fitbit
Reflect this in capabilities inside your strategy:
That’s all — no new route files needed. The unified router at /api/v1/providers/suunto/webhooks will route all POST and GET requests to your handler automatically.
GET /api/v1/providers/{provider}/webhooks → strategy.webhooks.handle_challenge(request) POST /api/v1/providers/{provider}/webhooks → strategy.webhooks.handle(request, body, db)