Sitemap

WHOOP in Flutter?

8 min readJul 23, 2026

--

Build a powerful health tracking app by integrating WHOOP wearable data using OAuth 2.0 authentication

Introduction

The WHOOP wearable device has become increasingly popular among fitness enthusiasts and athletes for its advanced physiological tracking capabilities. As a Flutter developer, integrating WHOOP’s API into your app can provide users with valuable insights into their recovery, sleep patterns, and overall health metrics.

In this comprehensive guide, I’ll walk you through the complete process of integrating WHOOP’s developer API into your Flutter application. We’ll cover OAuth 2.0 authentication, token management, and making API calls to fetch real user data.

Press enter or click to view image in full size
Connect Whoop in Flutter App

What You’ll Build

By the end of this tutorial, you’ll have:

  • A fully functional OAuth 2.0 authentication flow with WHOOP
  • Automatic token refresh mechanism
  • Ability to fetch recovery scores, sleep data, and cycle information
  • Clean, maintainable code structure using best practices

Prerequisites

Before we start, make sure you have:

  • Flutter SDK installed (minimum version 2.0)
  • A WHOOP developer account with API credentials
  • Basic understanding of Flutter and Dart
  • A code editor (VS Code or Android Studio)

Setting Up Your WHOOP Developer Account

First, you’ll need to register your application with WHOOP:

  1. Visit the WHOOP Developer Portal
  2. Create a new application
  3. Note down your Client ID and Client Secret
  4. Configure your redirect URI (we’ll use a custom URL scheme)

For this tutorial, we’ll use:

  • Client ID: Your application’s client ID
  • Client Secret: Your application’s client secret
  • Redirect URI: yourappscheme://whoop-auth

Step 1: Project Setup and Dependencies

Create a new Flutter project or add to your existing one. Add these dependencies to your pubspec.yaml:

yaml

dependencies:
flutter:
sdk: flutter
flutter_appauth: ^6.1.0
http: ^1.1.0
shared_preferences: ^2.2.2

Run flutter pub get to install the dependencies.

Platform-Specific Configuration

Android (android/app/src/main/AndroidManifest.xml):

xml

<activity android:name="net.openid.appauth.RedirectUriReceiverActivity">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="yourappscheme" android:host="whoop-auth" />
</intent-filter>
</activity>

iOS (ios/Runner/Info.plist):

xml

<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>yourappscheme</string>
</array>
</dict>
</array>

Step 2: Session Management

We need a central place to store and retrieve authentication tokens. Create a SessionManager class:

dart

import 'package:shared_preferences/shared_preferences.dart';
class SessionManager {
static const String _whoopAccessTokenKey = 'whoop_access_token';
static const String _whoopRefreshTokenKey = 'whoop_refresh_token';
static const String _whoopTokenExpiryKey = 'whoop_token_expiry';
static Future<void> setWhoopAccessToken(String token) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_whoopAccessTokenKey, token);
}
static String? getWhoopAccessToken() {
final prefs = SharedPreferences.getInstance();
return prefs.getString(_whoopAccessTokenKey);
}
static Future<void> setWhoopRefreshToken(String token) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_whoopRefreshTokenKey, token);
}
static String? getWhoopRefreshToken() {
final prefs = SharedPreferences.getInstance();
return prefs.getString(_whoopRefreshTokenKey);
}
static Future<void> setWhoopTokenExpiry(DateTime expiry) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_whoopTokenExpiryKey, expiry.toIso8601String());
}
static Future<DateTime?> getWhoopTokenExpiry() async {
final prefs = await SharedPreferences.getInstance();
final expiryStr = prefs.getString(_whoopTokenExpiryKey);
if (expiryStr != null) {
return DateTime.parse(expiryStr);
}
return null;
}
static Future<void> clearWhoopTokens() async {
final prefs = await SharedPreferences.getInstance();
await prefs.remove(_whoopAccessTokenKey);
await prefs.remove(_whoopRefreshTokenKey);
await prefs.remove(_whoopTokenExpiryKey);
}
}

Step 3: Authentication Service

Now, let’s implement the OAuth 2.0 authentication service. This handles the login flow and token refresh.

dart

import 'package:flutter/foundation.dart';
import 'package:flutter_appauth/flutter_appauth.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
class WhoopAuthService {
static const String clientId = 'YOUR_CLIENT_ID';
static const String redirectUrl = 'yourappscheme://whoop-auth';
static const String clientSecret = 'YOUR_CLIENT_SECRET';
static const AuthorizationServiceConfiguration _serviceConfiguration =
AuthorizationServiceConfiguration(
authorizationEndpoint: 'https://api.prod.whoop.com/oauth/oauth2/auth',
tokenEndpoint: 'https://api.prod.whoop.com/oauth/oauth2/token',
);
static const List<String> _scopes = [
'read:recovery',
'read:cycles',
'read:sleep',
'read:workout',
'read:profile',
'read:body_measurement',
'offline',
];
final FlutterAppAuth _appAuth = const FlutterAppAuth(); Future<String?> login() async {
try {
final AuthorizationResponse authResponse = await _appAuth.authorize(
AuthorizationRequest(
clientId,
redirectUrl,
serviceConfiguration: _serviceConfiguration,
scopes: _scopes,
),
);
if (authResponse.authorizationCode == null) {
debugPrint('Authorization code request failed');
return null;
}
final http.Response tokenResponse = await http.post(
Uri.parse('https://api.prod.whoop.com/oauth/oauth2/token'),
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: {
'grant_type': 'authorization_code',
'code': authResponse.authorizationCode!,
'redirect_uri': redirectUrl,
'client_id': clientId,
'client_secret': clientSecret,
},
);
if (tokenResponse.statusCode == 200) {
final Map<String, dynamic> data = jsonDecode(tokenResponse.body);
final String? accessToken = data['access_token'];
final String? refreshToken = data['refresh_token'];
final int? expiresIn = data['expires_in'];
if (accessToken != null) {
await SessionManager.setWhoopAccessToken(accessToken);
if (refreshToken != null) {
await SessionManager.setWhoopRefreshToken(refreshToken);
}
if (expiresIn != null) {
await SessionManager.setWhoopTokenExpiry(
DateTime.now().add(Duration(seconds: expiresIn)),
);
}
return accessToken;
}
}
return null;
} catch (e) {
debugPrint('Login error: $e');
return null;
}
}
Future<String?> refreshToken() async {
try {
final String? refreshToken = SessionManager.getWhoopRefreshToken();
if (refreshToken == null) {
await SessionManager.clearWhoopTokens();
return null;
}
final http.Response tokenResponse = await http.post(
Uri.parse('https://api.prod.whoop.com/oauth/oauth2/token'),
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: {
'grant_type': 'refresh_token',
'refresh_token': refreshToken,
'client_id': clientId,
'client_secret': clientSecret,
},
);
if (tokenResponse.statusCode == 200) {
final Map<String, dynamic> data = jsonDecode(tokenResponse.body);
final String? newAccessToken = data['access_token'];
final String? newRefreshToken = data['refresh_token'];
final int? expiresIn = data['expires_in'];
if (newAccessToken != null) {
await SessionManager.setWhoopAccessToken(newAccessToken);
if (newRefreshToken != null) {
await SessionManager.setWhoopRefreshToken(newRefreshToken);
}
if (expiresIn != null) {
await SessionManager.setWhoopTokenExpiry(
DateTime.now().add(Duration(seconds: expiresIn)),
);
}
return newAccessToken;
}
}
await SessionManager.clearWhoopTokens();
return null;
} catch (e) {
debugPrint('Refresh token error: $e');
return null;
}
}
Future<String?> getValidToken() async {
try {
final String? accessToken = SessionManager.getWhoopAccessToken();
final DateTime? expiry = await SessionManager.getWhoopTokenExpiry();

if (accessToken == null || expiry == null || DateTime.now().isAfter(expiry)) {
return await refreshToken();
}
return accessToken;
} catch (e) {
debugPrint('Error getting valid token: $e');
return null;
}
}
}

Step 4: API Service for Data Fetching

Now, let’s create the service that will interact with WHOOP’s developer API endpoints:

Get Yogx Technologies’s stories in your inbox

Join Medium for free to get updates from this writer.

dart

import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
class WhoopApiService {
static const String _baseUrl = 'https://api.prod.whoop.com/developer/v2';
Map<String, String> _headers(String token) => {
'Authorization': 'Bearer $token',
};
static String _iso(DateTime d) =>
d.toUtc().toIso8601String().replaceAll(RegExp(r'\.\d+'), '');
Future<Map<String, dynamic>> getCycles() {
final now = DateTime.now();
final start = _iso(now.subtract(const Duration(hours: 24)));
final end = _iso(now);
return _get('cycle', {'start': start, 'end': end});
}
Future<Map<String, dynamic>> getRecovery() {
final now = DateTime.now();
final start = _iso(now.subtract(const Duration(hours: 24)));
final end = _iso(now);
return _get('recovery', {'start': start, 'end': end});
}
Future<Map<String, dynamic>> getSleep() {
final now = DateTime.now();
final start = _iso(now.subtract(const Duration(hours: 24)));
final end = _iso(now);
return _get('activity/sleep', {'start': start, 'end': end});
}
Future<Map<String, dynamic>> _get(
String path, [
Map<String, String>? params,
]) async {
try {
final uri = Uri.parse('$_baseUrl/$path').replace(queryParameters: params);
String? accessToken = SessionManager.getWhoopAccessToken(); if (accessToken == null) {
throw Exception('Access token not found');
}
http.Response response = await http.get(
uri,
headers: _headers(accessToken),
);
if (response.statusCode == 401) {
final newToken = await WhoopAuthService().refreshToken();
if (newToken == null) {
throw Exception('Token refresh failed');
}
response = await http.get(uri, headers: _headers(newToken));
}
if (response.statusCode != 200) {
throw Exception(
'API error: ${response.statusCode} - ${response.body}',
);
}
return jsonDecode(response.body) as Map<String, dynamic>;
} catch (e) {
debugPrint('API call error: $e');
rethrow;
}
}
}

Step 5: Building the User Interface

Create a simple UI to display the fetched data:

dart

import 'package:flutter/material.dart';
class WhoopDataScreen extends StatefulWidget {
@override
_WhoopDataScreenState createState() => _WhoopDataScreenState();
}
class _WhoopDataScreenState extends State<WhoopDataScreen> {
final WhoopAuthService _authService = WhoopAuthService();
final WhoopApiService _apiService = WhoopApiService();

Map<String, dynamic>? _recoveryData;
Map<String, dynamic>? _sleepData;
Map<String, dynamic>? _cycleData;
bool _isLoading = false;
String? _errorMessage;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('WHOOP Data'),
actions: [
IconButton(
icon: Icon(Icons.refresh),
onPressed: _fetchData,
),
],
),
body: _buildBody(),
);
}
Widget _buildBody() {
if (_isLoading) {
return Center(child: CircularProgressIndicator());
}
if (_errorMessage != null) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.error, size: 64, color: Colors.red),
SizedBox(height: 16),
Text(_errorMessage!),
SizedBox(height: 16),
ElevatedButton(
onPressed: _handleLogin,
child: Text('Login with WHOOP'),
),
],
),
);
}
if (_recoveryData == null) {
return Center(
child: ElevatedButton(
onPressed: _handleLogin,
child: Text('Connect WHOOP'),
),
);
}
return ListView(
padding: EdgeInsets.all(16),
children: [
_buildRecoveryCard(),
SizedBox(height: 16),
_buildSleepCard(),
SizedBox(height: 16),
_buildCycleCard(),
],
);
}
Widget _buildRecoveryCard() {
final recovery = _recoveryData?['recovery']?[0] ?? {};
return Card(
child: Padding(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Recovery Score',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
SizedBox(height: 8),
Text(
'${recovery['score']?['recovery_score'] ?? 'N/A'}%',
style: TextStyle(fontSize: 32, fontWeight: FontWeight.bold),
),
SizedBox(height: 8),
Text('Status: ${recovery['score']?['recovery_status'] ?? 'N/A'}'),
],
),
),
);
}
Widget _buildSleepCard() {
final sleep = _sleepData?['sleep']?[0] ?? {};
return Card(
child: Padding(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Sleep Data',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
SizedBox(height: 8),
Text('Duration: ${sleep['sleep_duration'] ?? 'N/A'} minutes'),
Text('Efficiency: ${sleep['efficiency'] ?? 'N/A'}%'),
Text('Deep Sleep: ${sleep['deep_sleep_duration'] ?? 'N/A'} minutes'),
],
),
),
);
}
Widget _buildCycleCard() {
final cycle = _cycleData?['cycles']?[0] ?? {};
return Card(
child: Padding(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Physiological Cycle',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
SizedBox(height: 8),
Text('Strain: ${cycle['score']?['strain'] ?? 'N/A'}'),
Text('Max HR: ${cycle['score']?['max_heart_rate'] ?? 'N/A'} bpm'),
Text('Avg HR: ${cycle['score']?['avg_heart_rate'] ?? 'N/A'} bpm'),
],
),
),
);
}
Future<void> _handleLogin() async {
setState(() {
_isLoading = true;
_errorMessage = null;
});
try {
final token = await _authService.login();
if (token != null) {
await _fetchData();
} else {
setState(() {
_errorMessage = 'Login failed. Please try again.';
_isLoading = false;
});
}
} catch (e) {
setState(() {
_errorMessage = 'An error occurred: $e';
_isLoading = false;
});
}
}
Future<void> _fetchData() async {
setState(() {
_isLoading = true;
_errorMessage = null;
});
try {
final token = await _authService.getValidToken();
if (token == null) {
setState(() {
_errorMessage = 'Please login again';
_isLoading = false;
});
return;
}
final recovery = await _apiService.getRecovery();
final sleep = await _apiService.getSleep();
final cycles = await _apiService.getCycles();
setState(() {
_recoveryData = recovery;
_sleepData = sleep;
_cycleData = cycles;
_isLoading = false;
});
} catch (e) {
setState(() {
_errorMessage = 'Failed to fetch data: $e';
_isLoading = false;
});
}
}
}

Step 6: Main Application Integration

Update your main app file to include the WHOOP data screen:

dart

import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'WHOOP Integration Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: WhoopDataScreen(),
);
}
}

Understanding the Data Flow

Let’s break down how the integration works:

  1. Authentication Flow: When the user taps “Connect WHOOP”, the app launches the WHOOP authorization page. The user logs in and grants permissions. WHOOP redirects back to your app with an authorization code.
  2. Token Exchange: The app exchanges the authorization code for access and refresh tokens. These tokens are stored securely using shared preferences.
  3. Making API Calls: For each API request, the app uses the access token. If the token has expired (401 status code), it automatically refreshes using the refresh token.
  4. Data Display: The fetched data is parsed and displayed in a user-friendly format.

Error Handling and Best Practices

Token Management

  • Always check token expiry before making API calls
  • Implement automatic token refresh
  • Clear tokens when they’re invalid

User Experience

  • Show loading indicators during network operations
  • Provide clear error messages
  • Implement retry mechanisms for failed requests

Security

  • Never hardcode client secrets in client-side code
  • Use secure storage for tokens
  • Implement proper scope permissions

Testing Your Integration

To test your integration:

  1. Run the app on a physical device or emulator
  2. Tap “Connect WHOOP”
  3. Complete the authorization flow
  4. Verify that data appears on the screen
  5. Test the refresh mechanism by waiting for the token to expire

Common Issues and Solutions

Issue: Redirect URI doesn’t work
Solution: Ensure the URL scheme is properly configured in both Android and iOS manifests.

Issue: Token refresh fails
Solution: Verify that your app has offline access scope. Check that the refresh token is stored correctly.

Issue: API returns 401 errors
Solution: Implement the automatic refresh logic as shown in the code above.

Conclusion

Integrating WHOOP API into your Flutter app opens up a world of possibilities for health and fitness applications. With OAuth 2.0 authentication and proper token management, you can provide users with seamless access to their physiological data.

The code we’ve built together provides a solid foundation that you can extend with additional features like:

  • Historical data visualization
  • Workout tracking
  • User profile management
  • Data synchronization across devices

Remember to always respect user privacy and handle health data responsibly. Happy coding!

Resources for Further Learning

  • WHOOP Developer Documentation
  • Flutter OAuth 2.0 Best Practices
  • Health Data Privacy Guidelines
  • Flutter State Management Patterns

This guide was written for Flutter 3.0+ and the latest WHOOP API v2. Always check for API updates and deprecations when building production applications.

--

--

Yogx Technologies
Yogx Technologies
0 followers

YogX Technologies shares random tech articles, tutorials, insights, and updates on AI, software, cloud, DevOps, data, and emerging technologies.