Garmin Health API lets your Bolt.new app access fitness data — daily steps, heart rate, sleep, and activities — from Garmin wearables. The integration uses OAuth 1.0a (an older protocol) with a consumer key/secret. OAuth callbacks and Garmin's push-based webhook data delivery require deployment to Netlify or Bolt Cloud before testing. Plan to deploy early in development.
Building Fitness Dashboards with Garmin Health API
Garmin Health API provides access to detailed fitness data from Garmin GPS watches and fitness trackers — activities, heart rate, sleep, daily summaries, and stress data. It's the go-to source for serious athlete data, distinct from consumer-grade platforms like Fitbit.
Two technical challenges distinguish Garmin from simpler API integrations: OAuth 1.0a (request-signing on every call, not just a bearer token) and push-based data delivery (Garmin pings your webhook when data is ready, rather than you polling for it). Both require careful implementation, and the push delivery absolutely requires deployment before you can test the full sync flow.
Developer access requires registering for the Garmin Health API program — there's a form submission and waiting period before you receive credentials, unlike APIs that provide instant sandbox access.
Integration method
Garmin Health API uses OAuth 1.0a for authentication, which requires request signing on every API call rather than a simple Bearer token. All Garmin API calls must go through Next.js API routes — never from client-side code — because OAuth 1.0a signature generation requires your consumer secret to stay server-side. Garmin's push-based data delivery (pinging your webhook when new data is available) requires a publicly accessible URL, so you must deploy before testing data syncs.
Prerequisites
- A Bolt.new account with a Next.js project created
- A Garmin Health API developer account — register at developer.garmin.com/gc-developer-program/ (approval may take several business days)
- A Garmin consumer key and consumer secret received after developer program approval
- A Garmin Connect account with a device for testing
- Understanding that OAuth callback URL and webhook testing both require deployment to a live domain
Step-by-step guide
Register for Garmin Health API and Receive Credentials
Register for Garmin Health API and Receive Credentials
Go to developer.garmin.com/gc-developer-program/overview/ and click 'Get Started' to apply for API access. Unlike APIs with instant sandbox access, Garmin's developer program requires an application form where you describe your app's purpose, intended user base, and how you'll use the health data. Submit the form and expect to wait 3-5 business days for approval. Once approved, Garmin sends your consumer key and consumer secret by email or through their developer portal. The consumer key is a public identifier for your application (safe to include in certain OAuth flows, but best kept server-side). The consumer secret is the equivalent of a private API key — it signs every request your server makes to Garmin and must never appear in client-side code. In the Garmin developer portal, you'll also configure your application's callback URL. This is the URL Garmin redirects users to after they authorize your app — it must be a real, accessible URL on your deployed domain. Set a placeholder URL now (you can update it after deployment). Also note the list of data permissions (scopes) you're requesting — only request data types your app actually uses, as users see these on the consent screen.
Pro tip: Garmin's developer program is strict about commercial use. If you're building a commercial product that uses Garmin data, describe this clearly in your application — misrepresenting intended use can result in API access being revoked.
Expected result: You've received your Garmin consumer key and consumer secret and configured a callback URL placeholder in the developer portal.
Install OAuth 1.0a Library and Configure Environment Variables
Install OAuth 1.0a Library and Configure Environment Variables
OAuth 1.0a is significantly more complex than OAuth 2.0. Each API request requires a cryptographic signature generated from: the HTTP method, the URL, request parameters, a timestamp, a nonce (random string), your consumer key, and your consumer secret. The oauth-1.0a npm package handles this signing process for you. Install oauth-1.0a and its dependency crypto-js (for the HMAC-SHA1 hashing). You'll also need node-fetch or can use the native fetch API for making signed requests. Store all Garmin credentials server-side with no NEXT_PUBLIC_ prefix. The consumer secret signs every request — if it's exposed on the client, anyone could make API calls on behalf of your application.
Install the oauth-1.0a npm package. Create a lib/garmin-auth.ts file that exports: 1) A createOAuthHeader(method, url, params) function that generates an OAuth 1.0a Authorization header using GARMIN_CONSUMER_KEY and GARMIN_CONSUMER_SECRET environment variables. 2) A garminRequest(method, url, oauthToken?, oauthTokenSecret?) function that makes an authenticated request to the Garmin API with proper OAuth signing. Use TypeScript.
Paste this in Bolt.new chat
1# .env file — all Garmin credentials server-side only2GARMIN_CONSUMER_KEY=your_consumer_key3GARMIN_CONSUMER_SECRET=your_consumer_secret4GARMIN_API_BASE_URL=https://apis.garmin.com5GARMIN_REQUEST_TOKEN_URL=https://connectapi.garmin.com/oauth-service/oauth/request_token6GARMIN_AUTHORIZE_URL=https://connect.garmin.com/oauthConfirm7GARMIN_ACCESS_TOKEN_URL=https://connectapi.garmin.com/oauth-service/oauth/access_token8# Your deployed callback URL — update after deployment9GARMIN_CALLBACK_URL=https://your-app.netlify.app/api/auth/garmin/callbackPro tip: OAuth 1.0a has three steps: (1) get a request token, (2) redirect user to authorize, (3) exchange request token + verifier for access token. Each step uses different OAuth parameters. Keep a reference to the OAuth 1.0a spec open while building the flow.
Expected result: oauth-1.0a is installed. lib/garmin-auth.ts provides OAuth 1.0a signing functions. TypeScript compiles without errors.
Implement the Three-Legged OAuth 1.0a Flow
Implement the Three-Legged OAuth 1.0a Flow
OAuth 1.0a uses a three-step 'three-legged' flow. Unlike OAuth 2.0 where you redirect directly to the authorization URL, OAuth 1.0a requires an extra step: first obtain a temporary 'request token' from Garmin using your consumer credentials, then redirect the user to Garmin's authorization page with that request token, then exchange the returned verifier for a permanent access token. Step 1 (Request Token): Your server calls Garmin's request_token endpoint with OAuth 1.0a signing. Garmin returns an oauth_token and oauth_token_secret. Store these temporarily (in a session or database) — you'll need them in step 3. Step 2 (User Authorization): Redirect the user to Garmin's authorization URL with the oauth_token appended. The user sees Garmin's consent screen and approves access. Garmin redirects back to your callback URL with an oauth_verifier parameter. Step 3 (Access Token): Exchange the request token + oauth_verifier for a permanent access token. This access token + access token secret are what you store and use for all future API calls on behalf of this user. Critical: this entire flow requires a publicly accessible callback URL. It cannot be tested in Bolt's WebContainer preview.
Create the three OAuth 1.0a API routes for Garmin authentication: 1) /api/auth/garmin/start that gets a request token and redirects to Garmin's authorize URL. 2) /api/auth/garmin/callback that receives the oauth_verifier, exchanges it for a permanent access token, saves the access token and secret to the database, and redirects to /dashboard. 3) /api/auth/garmin/disconnect that revokes access and removes stored tokens. Use TypeScript and store tokens in the database.
Paste this in Bolt.new chat
1// app/api/auth/garmin/start/route.ts2import { NextResponse } from 'next/server';3import OAuth from 'oauth-1.0a';4import crypto from 'crypto';56const oauth = new OAuth({7 consumer: {8 key: process.env.GARMIN_CONSUMER_KEY ?? '',9 secret: process.env.GARMIN_CONSUMER_SECRET ?? '',10 },11 signature_method: 'HMAC-SHA1',12 hash_function(base_string, key) {13 return crypto.createHmac('sha1', key).update(base_string).digest('base64');14 },15});1617export async function GET(request: Request) {18 try {19 const requestTokenUrl = process.env.GARMIN_REQUEST_TOKEN_URL!;20 const callbackUrl = process.env.GARMIN_CALLBACK_URL!;2122 const requestData = {23 url: requestTokenUrl,24 method: 'POST' as const,25 data: { oauth_callback: callbackUrl },26 };27 const authHeader = oauth.toHeader(oauth.authorize(requestData));2829 const response = await fetch(requestTokenUrl, {30 method: 'POST',31 headers: { ...authHeader, 'Content-Type': 'application/x-www-form-urlencoded' },32 body: new URLSearchParams({ oauth_callback: callbackUrl }),33 });3435 if (!response.ok) throw new Error(`Request token failed: ${response.status}`);3637 const body = await response.text();38 const params = new URLSearchParams(body);39 const oauthToken = params.get('oauth_token');4041 if (!oauthToken) throw new Error('No oauth_token in response');4243 // Store oauth_token_secret temporarily (session/db) — needed in callback44 const oauthTokenSecret = params.get('oauth_token_secret');45 // TODO: save oauthTokenSecret associated with oauthToken in your session/db4647 const authorizeUrl = `${process.env.GARMIN_AUTHORIZE_URL}?oauth_token=${oauthToken}`;48 return NextResponse.redirect(authorizeUrl);49 } catch (error) {50 console.error('Garmin OAuth start error:', error);51 return NextResponse.redirect(new URL('/?error=garmin_auth_failed', request.url));52 }53}Pro tip: OAuth 1.0a requires storing the temporary request token secret between step 1 and step 3. A Redis cache, database record, or encrypted cookie work well. The request token typically expires in 15-30 minutes.
Expected result: Visiting /api/auth/garmin/start redirects to Garmin's authorization page. After authorization, Garmin redirects to your callback URL. The complete flow only works on a deployed domain.
Fetch Fitness Data from Garmin Health API
Fetch Fitness Data from Garmin Health API
With OAuth access tokens stored for a user, you can now call Garmin's Health API endpoints. The most useful endpoints for fitness apps are: the Daily Summary endpoint (steps, calories, intensity minutes, floor climbs), the Activities endpoint (workouts with GPS data), the Sleep endpoint (sleep stages and scores), and the Heart Rate endpoint (detailed heart rate data throughout the day). All requests are signed with OAuth 1.0a using the user's access token and access token secret along with your consumer credentials. The garminRequest() function from lib/garmin-auth.ts handles this signing. Garmin returns data in summary and detail formats. Daily summaries aggregate a full day's metrics into a single record. Activity summaries provide workout-level data. FIT files (Garmin's binary activity format) contain the complete GPS track and sensor data for a workout — parse them with the fit-file-parser npm package if you need detailed workout data.
Create a /api/garmin/daily-summary route that accepts startDate and endDate query parameters and fetches the user's daily wellness summaries from the Garmin Health API. Return an array with date, steps, activeKilocalories, floorsClimbed, intensityMinutes, and averageStressLevel for each day. Also create a /api/garmin/activities route that returns the last 20 activities with activityId, activityType, startTimeLocal, durationInSeconds, and distanceInMeters.
Paste this in Bolt.new chat
1// app/api/garmin/daily-summary/route.ts2import { NextResponse } from 'next/server';3import { garminRequest } from '@/lib/garmin-auth';45export async function GET(request: Request) {6 const url = new URL(request.url);7 const startDate = url.searchParams.get('startDate') ?? new Date(Date.now() - 7 * 86400000).toISOString().split('T')[0];8 const endDate = url.searchParams.get('endDate') ?? new Date().toISOString().split('T')[0];910 // Get user's Garmin OAuth tokens from session/database11 // Replace with your actual token retrieval12 const userTokens = { token: 'USER_OAUTH_TOKEN', secret: 'USER_OAUTH_SECRET' };1314 try {15 const apiUrl = `${process.env.GARMIN_API_BASE_URL}/wellness-api/rest/dailies?uploadStartTimeInSeconds=&uploadEndTimeInSeconds=`;16 // Garmin uses upload time ranges for daily summaries17 // Convert dates to epoch seconds for the API18 const startEpoch = Math.floor(new Date(startDate).getTime() / 1000);19 const endEpoch = Math.floor(new Date(endDate + 'T23:59:59').getTime() / 1000);2021 const summariesUrl = `${process.env.GARMIN_API_BASE_URL}/wellness-api/rest/dailies?uploadStartTimeInSeconds=${startEpoch}&uploadEndTimeInSeconds=${endEpoch}`;2223 const data = await garminRequest('GET', summariesUrl, userTokens.token, userTokens.secret);2425 const summaries = (data?.dailies ?? []).map((day: Record<string, unknown>) => ({26 date: day.calendarDate,27 steps: day.totalSteps ?? 0,28 activeKilocalories: day.activeKilocalories ?? 0,29 floorsClimbed: day.floorsClimbed ?? 0,30 intensityMinutes: (Number(day.moderateIntensityMinutes ?? 0) + Number(day.vigorousIntensityMinutes ?? 0) * 2),31 averageStressLevel: day.averageStressLevel ?? null,32 }));3334 return NextResponse.json({ summaries });35 } catch (error) {36 console.error('Garmin daily summary error:', error);37 return NextResponse.json({ error: 'Failed to fetch Garmin data' }, { status: 500 });38 }39}Pro tip: Garmin's Health API uses Unix epoch timestamps (seconds) for date ranges, not date strings. Always convert date strings to epoch seconds when building query parameters for the API.
Expected result: The /api/garmin/daily-summary endpoint returns daily wellness data for authenticated users. Test after deploying and completing the OAuth flow with a real Garmin account.
Set Up Push Webhooks for Real-Time Data (Deployment Required)
Set Up Push Webhooks for Real-Time Data (Deployment Required)
Garmin's data delivery model is push-based — when a user syncs their device, Garmin sends the data to your registered webhook URL. Your app doesn't poll for new data; instead, you receive it automatically. This architecture is excellent for production but requires a publicly accessible server, making it impossible to test in Bolt's WebContainer. After deploying your app, register your webhook URL in the Garmin developer portal: provide the HTTPS URL of your webhook endpoint (e.g., https://your-app.netlify.app/api/webhooks/garmin). Garmin sends a ping with a verification challenge when you register — your endpoint must echo back the challenge value to confirm ownership. Once registered, Garmin sends POST requests to your webhook when users sync activities, sleep data, daily summaries, and health snapshots. Each push contains a summaries array with the data payload. Process it quickly and return 200 — if your endpoint doesn't respond within Garmin's timeout, it marks the push as failed and retries.
Create a /api/webhooks/garmin route that handles Garmin's push data delivery. For GET requests (Garmin's registration ping), return the query parameter value as plain text for domain verification. For POST requests (data pushes), parse the JSON body and process each item in the 'dailies', 'activities', 'sleeps', and 'epochs' arrays — save the data to a database table keyed by user ID and date. Always return 200.
Paste this in Bolt.new chat
1// app/api/webhooks/garmin/route.ts2import { NextResponse } from 'next/server';34// GET — Garmin sends this to verify your webhook URL during registration5export async function GET(request: Request) {6 const url = new URL(request.url);7 const challenge = url.searchParams.get('challenge');8 // Return the challenge value as plain text to verify domain ownership9 if (challenge) {10 return new Response(challenge, {11 status: 200,12 headers: { 'Content-Type': 'text/plain' },13 });14 }15 return new Response('Garmin webhook endpoint active', { status: 200 });16}1718// POST — Garmin sends fitness data when users sync their devices19export async function POST(request: Request) {20 try {21 const body = await request.json();2223 // Process each data type Garmin pushes24 if (body.dailies) {25 for (const daily of body.dailies) {26 console.log('Daily summary push:', daily.userId, daily.calendarDate);27 // Save to database: upsert by userId + calendarDate28 }29 }30 if (body.activities) {31 for (const activity of body.activities) {32 console.log('Activity push:', activity.userId, activity.activityId);33 // Save activity summary to database34 }35 }36 if (body.sleeps) {37 for (const sleep of body.sleeps) {38 console.log('Sleep data push:', sleep.userId, sleep.calendarDate);39 // Save sleep record to database40 }41 }4243 // CRITICAL: Return 200 immediately44 // Garmin retries if response takes too long45 return NextResponse.json({ status: 'received' }, { status: 200 });46 } catch (error) {47 console.error('Garmin webhook error:', error);48 return NextResponse.json({ status: 'error' }, { status: 200 });49 }50}Pro tip: Register your webhook URL in the Garmin developer portal only after deploying. Garmin sends a GET request with a challenge parameter to verify your URL — your GET handler must echo back the challenge value or the registration will fail.
Expected result: The webhook endpoint is deployed and registered with Garmin. When a test user syncs their Garmin device, data appears in your server logs and is saved to the database.
Common use cases
Athlete Training Dashboard
Build a training log that pulls activities from Garmin Connect, displaying run/ride metrics, weekly training load, heart rate zones, and trends over time. Athletes can see their data in a cleaner, more customizable view than Garmin's native app, with additional analysis and visualizations.
Build a fitness dashboard for Garmin users. After OAuth login, fetch the user's recent activities from the Garmin Activities API (last 30 days) and display them in a table with date, activity type, duration, distance, and average heart rate. Show a weekly summary card with total distance and time. All Garmin API calls should go through Next.js API routes using OAuth 1.0a signing.
Copy this prompt to try it in Bolt.new
Sleep Quality Tracker
Create a sleep analysis app that reads Garmin's detailed sleep data — sleep stages, REM cycles, sleep score, and overnight heart rate — and visualizes trends over weeks or months. Garmin's sleep data is more detailed than most wearables, making it valuable for sleep optimization tools.
Create a sleep tracking dashboard using Garmin Health API. Fetch sleep data for the last 30 days via the Daily Summary endpoint. Display a chart showing sleep duration and sleep score per night, with a breakdown of light/deep/REM stages. Show average values for the month. Include a webhook endpoint for receiving Garmin's sleep data push.
Copy this prompt to try it in Bolt.new
Coaching Platform with Client Data Access
Build a platform where athletes authorize a coach to view their Garmin data. The coach can see multiple athletes' training loads, recovery scores, and recent activities in a unified dashboard without requiring athletes to manually share data.
Build a coach-athlete platform where athletes connect their Garmin account via OAuth. Store the OAuth tokens in a database keyed by user ID. Create a coach dashboard that displays all connected athletes with their latest activity summary and 7-day training load. Coaches see aggregated data without knowing athlete credentials.
Copy this prompt to try it in Bolt.new
Troubleshooting
OAuth 1.0a signature verification fails with '401 Unauthorized' even with correct credentials
Cause: OAuth 1.0a signatures are time-sensitive — the signature includes a timestamp and Garmin validates that it's within an acceptable time window. Incorrect system time on the server causes signature failures. Also, parameter encoding must be exact — special characters in the URL must be percent-encoded correctly.
Solution: Ensure your server's system time is synchronized (NTP). Verify the oauth-1.0a library is generating signatures for the exact URL being called (including query parameters). Log the base string generated by the library for debugging.
Webhook registration fails with 'domain verification failed'
Cause: Garmin's GET challenge during webhook registration requires your endpoint to return the challenge value as plain text. Any other response — including JSON — causes verification to fail. The endpoint must be deployed and accessible before attempting registration.
Solution: Ensure your GET handler returns the challenge parameter value as plain text (not JSON). Test the endpoint manually: curl 'https://your-app.netlify.app/api/webhooks/garmin?challenge=testvalue' should return 'testvalue' as the response body.
1// Correct — plain text response2return new Response(challenge, { status: 200, headers: { 'Content-Type': 'text/plain' } });3// WRONG — JSON wrapping fails verification4// return NextResponse.json({ challenge });Daily summary endpoint returns empty array even after users sync their devices
Cause: Garmin's pull API uses upload time ranges (when data was uploaded to Garmin servers), not activity time ranges. If your time range doesn't match when the upload occurred, you'll get no results.
Solution: Use a broad upload time range — the last 7 days — rather than trying to match exact activity dates. Garmin's push webhooks are more reliable for getting current data than polling the pull API.
1// Use uploadEndTimeInSeconds as current time, uploadStartTimeInSeconds as 7 days ago2const now = Math.floor(Date.now() / 1000);3const sevenDaysAgo = now - (7 * 24 * 60 * 60);4const apiUrl = `...?uploadStartTimeInSeconds=${sevenDaysAgo}&uploadEndTimeInSeconds=${now}`;Best practices
- Store the OAuth 1.0a consumer secret strictly server-side — it signs every API request and must never appear in client-side code
- Use the push webhook model as your primary data ingestion method rather than polling the pull API — Garmin's architecture is designed around push delivery
- Deploy before testing OAuth callbacks or webhooks — Bolt's WebContainer cannot receive incoming connections from Garmin's servers
- Cache Garmin access tokens in your database and only call Garmin APIs when necessary — Garmin's API has rate limits per consumer key
- Always return HTTP 200 from webhook handlers immediately, even if processing fails — Garmin's retry logic floods your endpoint if it receives failures
- Test with a real Garmin device after deploying — the Garmin developer portal doesn't have a built-in sandbox, so you need actual device syncs to test data flows
- Implement the GET challenge handler for webhook registration before attempting to register your URL in the developer portal
Alternatives
Fitbit has a more developer-friendly API with OAuth 2.0 (simpler than Garmin's OAuth 1.0a), instant developer access, and a simulator for testing without a physical device.
Google Fit aggregates data from multiple wearables and phone sensors via OAuth 2.0 — simpler integration and broader device support, though with less data depth than Garmin for serious athletes.
Apple HealthKit aggregates all iOS health data including Garmin imports — better for building iOS health apps that need data from multiple sources including Apple Watch.
Withings focuses on health metrics (weight, blood pressure, sleep) rather than fitness performance — better for health monitoring apps than sports performance tracking.
Frequently asked questions
How do I connect Bolt.new to the Garmin Health API?
Register for the Garmin Health API developer program (requires approval, takes a few days), receive your consumer key and secret, install oauth-1.0a, and build Next.js API routes that implement the three-legged OAuth 1.0a flow. All API calls must be signed with OAuth 1.0a. Deploy to Netlify or Bolt Cloud first, then test the OAuth flow and webhook registration using your deployed URL.
Why does Garmin use OAuth 1.0a instead of the newer OAuth 2.0?
Garmin's Health API was built before OAuth 2.0 became the dominant standard, and they haven't migrated. OAuth 1.0a is more complex to implement (every request requires cryptographic signing) but is still secure. The oauth-1.0a npm package handles the complexity — you provide your credentials and the library generates the signatures.
Can I test the Garmin integration in Bolt's WebContainer preview?
Partially. The OAuth signature generation logic can be tested in preview with mock data. However, the OAuth callback (step 3 of the authorization flow) requires a registered redirect URI on a real domain, and Garmin's push webhooks require your server to receive incoming connections. Both require deployment. Plan to deploy to Netlify or Bolt Cloud early in development to test the complete flow.
Does the Garmin API provide real-time data, or is it delayed?
Garmin data is not real-time — it syncs when the user manually syncs their device to Garmin Connect (via Bluetooth to their phone, then to Garmin's servers). Typical sync frequency is daily, though users can sync more frequently. The push webhook fires when data reaches Garmin's servers, usually within minutes of a device sync.
Is there a Garmin API sandbox for testing without a physical device?
Garmin does not provide a simulator or sandbox environment. You need a real Garmin device and a Garmin Connect account to test data syncs. For development, you can use static mock data to build and style your UI, then connect the real API after deploying and completing OAuth authorization with your own Garmin account.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation