Skip to main content
RapidDev - Software Development Agency
bolt-ai-integrationsBolt Chat + API Route

How to Integrate Bolt.new with Garmin Connect API

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.

What you'll learn

  • How to register for the Garmin Health API developer program and obtain a consumer key and secret
  • How OAuth 1.0a differs from OAuth 2.0 and how to implement request signing in a Next.js API route
  • How to build the three-legged OAuth flow for user authorization
  • How to fetch daily summaries, activity files, heart rate data, and sleep records
  • Why Garmin's push API requires deployment and how to set up webhook endpoints to receive data
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate14 min read45 minutesOtherApril 2026RapidDev Engineering Team
TL;DR

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

Bolt Chat + API Route

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

1

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.

2

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.

Bolt.new Prompt

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

.env
1# .env file all Garmin credentials server-side only
2GARMIN_CONSUMER_KEY=your_consumer_key
3GARMIN_CONSUMER_SECRET=your_consumer_secret
4GARMIN_API_BASE_URL=https://apis.garmin.com
5GARMIN_REQUEST_TOKEN_URL=https://connectapi.garmin.com/oauth-service/oauth/request_token
6GARMIN_AUTHORIZE_URL=https://connect.garmin.com/oauthConfirm
7GARMIN_ACCESS_TOKEN_URL=https://connectapi.garmin.com/oauth-service/oauth/access_token
8# Your deployed callback URL update after deployment
9GARMIN_CALLBACK_URL=https://your-app.netlify.app/api/auth/garmin/callback

Pro 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.

3

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.

Bolt.new Prompt

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

app/api/auth/garmin/start/route.ts
1// app/api/auth/garmin/start/route.ts
2import { NextResponse } from 'next/server';
3import OAuth from 'oauth-1.0a';
4import crypto from 'crypto';
5
6const 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});
16
17export async function GET(request: Request) {
18 try {
19 const requestTokenUrl = process.env.GARMIN_REQUEST_TOKEN_URL!;
20 const callbackUrl = process.env.GARMIN_CALLBACK_URL!;
21
22 const requestData = {
23 url: requestTokenUrl,
24 method: 'POST' as const,
25 data: { oauth_callback: callbackUrl },
26 };
27 const authHeader = oauth.toHeader(oauth.authorize(requestData));
28
29 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 });
34
35 if (!response.ok) throw new Error(`Request token failed: ${response.status}`);
36
37 const body = await response.text();
38 const params = new URLSearchParams(body);
39 const oauthToken = params.get('oauth_token');
40
41 if (!oauthToken) throw new Error('No oauth_token in response');
42
43 // Store oauth_token_secret temporarily (session/db) — needed in callback
44 const oauthTokenSecret = params.get('oauth_token_secret');
45 // TODO: save oauthTokenSecret associated with oauthToken in your session/db
46
47 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.

4

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.

Bolt.new Prompt

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

app/api/garmin/daily-summary/route.ts
1// app/api/garmin/daily-summary/route.ts
2import { NextResponse } from 'next/server';
3import { garminRequest } from '@/lib/garmin-auth';
4
5export 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];
9
10 // Get user's Garmin OAuth tokens from session/database
11 // Replace with your actual token retrieval
12 const userTokens = { token: 'USER_OAUTH_TOKEN', secret: 'USER_OAUTH_SECRET' };
13
14 try {
15 const apiUrl = `${process.env.GARMIN_API_BASE_URL}/wellness-api/rest/dailies?uploadStartTimeInSeconds=&uploadEndTimeInSeconds=`;
16 // Garmin uses upload time ranges for daily summaries
17 // Convert dates to epoch seconds for the API
18 const startEpoch = Math.floor(new Date(startDate).getTime() / 1000);
19 const endEpoch = Math.floor(new Date(endDate + 'T23:59:59').getTime() / 1000);
20
21 const summariesUrl = `${process.env.GARMIN_API_BASE_URL}/wellness-api/rest/dailies?uploadStartTimeInSeconds=${startEpoch}&uploadEndTimeInSeconds=${endEpoch}`;
22
23 const data = await garminRequest('GET', summariesUrl, userTokens.token, userTokens.secret);
24
25 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 }));
33
34 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.

5

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.

Bolt.new Prompt

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

app/api/webhooks/garmin/route.ts
1// app/api/webhooks/garmin/route.ts
2import { NextResponse } from 'next/server';
3
4// GET — Garmin sends this to verify your webhook URL during registration
5export 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 ownership
9 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}
17
18// POST — Garmin sends fitness data when users sync their devices
19export async function POST(request: Request) {
20 try {
21 const body = await request.json();
22
23 // Process each data type Garmin pushes
24 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 + calendarDate
28 }
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 database
34 }
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 database
40 }
41 }
42
43 // CRITICAL: Return 200 immediately
44 // Garmin retries if response takes too long
45 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.

Bolt.new Prompt

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.

Bolt.new Prompt

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.

Bolt.new Prompt

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.

typescript
1// Correct — plain text response
2return new Response(challenge, { status: 200, headers: { 'Content-Type': 'text/plain' } });
3// WRONG — JSON wrapping fails verification
4// 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.

typescript
1// Use uploadEndTimeInSeconds as current time, uploadStartTimeInSeconds as 7 days ago
2const 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

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.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.