Skip to main content

How to Add a New Provider Integration

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.

Architecture Overview

Before diving into implementation, understand the main components:
  1. Strategy - Defines the provider’s identity, capabilities, and wires together all components
  2. Strategy Factory - Central management of strategy instantiation (used by routes)
  3. OAuth Handler - Manages authentication flow (if provider uses cloud API)
  4. Workouts Handler - Fetches and normalizes workout/activity data
  5. 247 Data Handler - Fetches continuous health metrics (sleep, recovery, HR, etc.)
  6. Webhook Handler - Receives and processes incoming push events from the provider
Each provider declares its data delivery modes via ProviderCapabilities:
webhook_stream and webhook_ping are mutually exclusive. Use webhook_stream when the provider embeds the complete record in the webhook body (Garmin, Suunto). Use webhook_ping when the webhook is only a trigger and you must call the REST API to retrieve the actual data (Oura, Strava, Fitbit, Polar).
Custom providers architecture showing Strategy, OAuth, Workouts, 247 Data, and Webhook Handler components

Prerequisites

Before starting, gather the following information about your provider:
  • 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
  • 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
  • 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.

Step 1: Create Provider Directory Structure

Create a new directory for your provider in backend/app/services/providers/. For a provider named Suunto, create:
Use lowercase for the provider name in directory and file names to maintain consistency with existing providers.

Step 2: Implement the Strategy Class

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:

Key Points:

  • 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
  • coverage: declares what data the provider emits; pass through all four constants from coverage.py (see below)
  • 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
Provider strategy class diagram showing capabilities, components, and their relationships
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.

Declare data coverage (coverage.py)

coverage.py is the single source of truth for what your provider emits. It powers the public GET /api/v1/meta/coverage endpoint and the Data Coverage matrix, and the coverage property above reads from it. Create backend/app/services/providers/suunto/coverage.py:
Rules:
  • Move any key → SeriesType mapping that lives in data_247.py/workouts.py into coverage.py and import it back, so the mapping is defined once and TIMESERIES is derived from it.
  • Single inline series_type=SeriesType.X emissions may stay in the handler, but X must appear in TIMESERIES.
  • Declare all four constants; use frozenset() for layers your provider doesn’t emit (e.g. a workouts-only provider has an empty TIMESERIES).
A guard test scans your implementation and fails if it emits a SeriesType or sets an EventRecordDetail field not declared here — and asserts the strategy’s coverage property exposes all four constants:

Step 3: Implement OAuth Handler (PULL providers)

If your provider uses OAuth 2.0 for authentication, implement the OAuth handler. Create backend/app/services/providers/suunto/oauth.py:
Here you can also create all provider-specific methods, like _register_user in Polar’s case.

Configuration Options:

use_pkce

Set to True if provider requires PKCE (Proof Key for Code Exchange). Garmin enforces PKCE, Polar and Suunto don’t.

auth_method

  • BASIC_AUTH: Credentials in Authorization header (Polar, Suunto)
  • BODY: Credentials in request body (Garmin)

Add Environment Variables:

Add your OAuth credentials to .env:
And update backend/app/config.py:

Step 4: Implement Workouts Handler

The workouts handler fetches and normalizes workout data from the provider’s API. Create backend/app/services/providers/suunto/workouts.py:

Key Methods to Implement:

1

_normalize_workout()

Most important! Convert provider’s data format to OpenWearables unified schema.
2

_extract_dates()

Handle provider-specific timestamp formats (Unix, ISO 8601, custom strings)
3

_build_metrics()

Creates statistics for Workout.
4

_build_bundles()

Optimize query by bundling workout records into packages.
5

load_data()

Main sync method that orchestrates fetching and saving data
There are also utils modules, like app/backend/services/providers/api_client.py, which provides utilities for making oauth api requests.

Step 5: Create Workout Type Mapping

Create a mapping file to convert provider-specific workout types to unified types. Create backend/app/constants/workout_types/suunto.py:
Review the existing unified workout types in your system before mapping. You may need to add new unified types to accommodate provider-specific activities.

Step 6: Register Provider in Factory

Add your new provider to the factory so it can be instantiated by the system. Edit backend/app/services/providers/factory.py:
Factory will be used by routes endpoints to fetch correct strategy.

Step 7: Add Provider to Schema Enums

Update the ProviderName enum to include your new provider. Edit backend/app/schemas/oauth.py:
This enables:
  • Type validation in API endpoints
  • Auto-generated API documentation with provider options
  • Enum-based routing

Step 8: Test Your Integration

Now test your implementation with these steps:

1. Test OAuth Flow (if applicable)

Visit the authorization URL in your browser, authorize, and verify the callback works.

2. Test Data Sync

3. Verify Database

Check that workouts are saved correctly:

4. Check Logs

Monitor logs for errors:
If you started the API outside Docker, follow the terminal where the local server is running.

Step 9 (Optional): Implement Webhook Handler (PUSH flow)

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.

Understand the delivery mode

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:

Create the webhook handler

Create backend/app/services/providers/suunto/webhook_handler.py:
BaseWebhookHandler provides two signature-verification helpers so you never reimplement cryptographic primitives:
  • _verify_hmac_sha256(secret, body, provided_signature) — for HMAC-SHA256 providers (Oura, Fitbit)
  • _verify_token(expected, provided) — for plain shared-secret header/query-param verification

Wire the handler into 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}/webhooksstrategy.webhooks.handle_challenge(request)
POST /api/v1/providers/{provider}/webhooksstrategy.webhooks.handle(request, body, db)

Automatic webhook registration (optional)

Some providers expose an API to register webhook subscriptions programmatically (e.g. Oura). For these, you can have subscriptions registered automatically whenever an admin switches the provider’s live sync mode to webhook — no manual setup in the developer portal required. To enable this:
  1. Set webhook_registration_api=True in your strategy’s ProviderCapabilities.
  2. Override register_webhooks(callback_url) in your strategy to call your provider’s subscription registration API. It should be idempotent — skip existing subscriptions, create missing ones.
When an admin switches the sync mode to webhook via PUT /api/v1/oauth/providers/{provider}, the platform automatically dispatches a Celery task (register_provider_webhooks) that calls strategy.register_webhooks(callback_url) in the background. The callback URL is derived from API_BASE_URL: {API_BASE_URL}/api/v1/providers/{provider}/webhooks.
If webhook_registration_api=False (the default), switching sync mode to webhook only updates the database — you must register subscriptions manually in the provider’s developer portal pointing to {API_BASE_URL}/api/v1/providers/{provider}/webhooks.

Troubleshooting

Add detailed logging in _normalize_workout to inspect raw data structure. Compare against provider’s API documentation.
Implement duplicate detection in _save_workout based on provider_id. Check if workout with same provider_id already exists.
Add missing types to your mapping file. Consider adding a fallback type (“other”) and logging unmapped types for future updates.

Summary Checklist

Use this checklist to ensure you’ve completed all steps:
  • Created provider directory structure (strategy.py, oauth.py, workouts.py)
  • Implemented ProviderStrategy with required properties and capabilities declaration
  • Implemented ProviderOAuth with endpoints, credentials, and user info fetch
  • Implemented ProviderWorkouts with normalization logic
  • Implemented ProviderWebhookHandler extending BaseWebhookHandler (if webhook_stream or webhook_ping)
  • Wired self.webhooks in strategy (or left as None if no webhooks)
  • Created workout type mapping file
  • Registered provider in ProviderFactory
  • Added provider to ProviderName enum
  • Added provider icon to static assets
  • Added environment variables to .env and config.py
  • Tested OAuth flow end-to-end
  • Tested data synchronization
  • Verified data in database
  • Tested webhook delivery at POST /api/v1/providers/{provider}/webhooks (if applicable)
  • Tested subscription verification at GET /api/v1/providers/{provider}/webhooks (if applicable)
  • If webhook_registration_api=True: implemented register_webhooks() in strategy and verified auto-registration fires on sync mode switch
  • Added error handling and logging
  • Updated API documentation
Congratulations! You’ve successfully integrated a new provider into OpenWearables. 🎉