# How to Integrate Bolt.new with Garmin Connect API

- Tool: Bolt.new
- Difficulty: Intermediate
- Time required: 45 minutes
- Last updated: April 2026

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

## Before you start

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

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

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

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

```
// app/api/auth/garmin/start/route.ts
import { NextResponse } from 'next/server';
import OAuth from 'oauth-1.0a';
import crypto from 'crypto';

const oauth = new OAuth({
  consumer: {
    key: process.env.GARMIN_CONSUMER_KEY ?? '',
    secret: process.env.GARMIN_CONSUMER_SECRET ?? '',
  },
  signature_method: 'HMAC-SHA1',
  hash_function(base_string, key) {
    return crypto.createHmac('sha1', key).update(base_string).digest('base64');
  },
});

export async function GET(request: Request) {
  try {
    const requestTokenUrl = process.env.GARMIN_REQUEST_TOKEN_URL!;
    const callbackUrl = process.env.GARMIN_CALLBACK_URL!;

    const requestData = {
      url: requestTokenUrl,
      method: 'POST' as const,
      data: { oauth_callback: callbackUrl },
    };
    const authHeader = oauth.toHeader(oauth.authorize(requestData));

    const response = await fetch(requestTokenUrl, {
      method: 'POST',
      headers: { ...authHeader, 'Content-Type': 'application/x-www-form-urlencoded' },
      body: new URLSearchParams({ oauth_callback: callbackUrl }),
    });

    if (!response.ok) throw new Error(`Request token failed: ${response.status}`);

    const body = await response.text();
    const params = new URLSearchParams(body);
    const oauthToken = params.get('oauth_token');

    if (!oauthToken) throw new Error('No oauth_token in response');

    // Store oauth_token_secret temporarily (session/db) — needed in callback
    const oauthTokenSecret = params.get('oauth_token_secret');
    // TODO: save oauthTokenSecret associated with oauthToken in your session/db

    const authorizeUrl = `${process.env.GARMIN_AUTHORIZE_URL}?oauth_token=${oauthToken}`;
    return NextResponse.redirect(authorizeUrl);
  } catch (error) {
    console.error('Garmin OAuth start error:', error);
    return NextResponse.redirect(new URL('/?error=garmin_auth_failed', request.url));
  }
}
```

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

```
// app/api/garmin/daily-summary/route.ts
import { NextResponse } from 'next/server';
import { garminRequest } from '@/lib/garmin-auth';

export async function GET(request: Request) {
  const url = new URL(request.url);
  const startDate = url.searchParams.get('startDate') ?? new Date(Date.now() - 7 * 86400000).toISOString().split('T')[0];
  const endDate = url.searchParams.get('endDate') ?? new Date().toISOString().split('T')[0];

  // Get user's Garmin OAuth tokens from session/database
  // Replace with your actual token retrieval
  const userTokens = { token: 'USER_OAUTH_TOKEN', secret: 'USER_OAUTH_SECRET' };

  try {
    const apiUrl = `${process.env.GARMIN_API_BASE_URL}/wellness-api/rest/dailies?uploadStartTimeInSeconds=&uploadEndTimeInSeconds=`;
    // Garmin uses upload time ranges for daily summaries
    // Convert dates to epoch seconds for the API
    const startEpoch = Math.floor(new Date(startDate).getTime() / 1000);
    const endEpoch = Math.floor(new Date(endDate + 'T23:59:59').getTime() / 1000);

    const summariesUrl = `${process.env.GARMIN_API_BASE_URL}/wellness-api/rest/dailies?uploadStartTimeInSeconds=${startEpoch}&uploadEndTimeInSeconds=${endEpoch}`;

    const data = await garminRequest('GET', summariesUrl, userTokens.token, userTokens.secret);

    const summaries = (data?.dailies ?? []).map((day: Record<string, unknown>) => ({
      date: day.calendarDate,
      steps: day.totalSteps ?? 0,
      activeKilocalories: day.activeKilocalories ?? 0,
      floorsClimbed: day.floorsClimbed ?? 0,
      intensityMinutes: (Number(day.moderateIntensityMinutes ?? 0) + Number(day.vigorousIntensityMinutes ?? 0) * 2),
      averageStressLevel: day.averageStressLevel ?? null,
    }));

    return NextResponse.json({ summaries });
  } catch (error) {
    console.error('Garmin daily summary error:', error);
    return NextResponse.json({ error: 'Failed to fetch Garmin data' }, { status: 500 });
  }
}
```

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

```
// app/api/webhooks/garmin/route.ts
import { NextResponse } from 'next/server';

// GET — Garmin sends this to verify your webhook URL during registration
export async function GET(request: Request) {
  const url = new URL(request.url);
  const challenge = url.searchParams.get('challenge');
  // Return the challenge value as plain text to verify domain ownership
  if (challenge) {
    return new Response(challenge, {
      status: 200,
      headers: { 'Content-Type': 'text/plain' },
    });
  }
  return new Response('Garmin webhook endpoint active', { status: 200 });
}

// POST — Garmin sends fitness data when users sync their devices
export async function POST(request: Request) {
  try {
    const body = await request.json();

    // Process each data type Garmin pushes
    if (body.dailies) {
      for (const daily of body.dailies) {
        console.log('Daily summary push:', daily.userId, daily.calendarDate);
        // Save to database: upsert by userId + calendarDate
      }
    }
    if (body.activities) {
      for (const activity of body.activities) {
        console.log('Activity push:', activity.userId, activity.activityId);
        // Save activity summary to database
      }
    }
    if (body.sleeps) {
      for (const sleep of body.sleeps) {
        console.log('Sleep data push:', sleep.userId, sleep.calendarDate);
        // Save sleep record to database
      }
    }

    // CRITICAL: Return 200 immediately
    // Garmin retries if response takes too long
    return NextResponse.json({ status: 'received' }, { status: 200 });
  } catch (error) {
    console.error('Garmin webhook error:', error);
    return NextResponse.json({ status: 'error' }, { status: 200 });
  }
}
```

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

## 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

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

Prompt example:

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

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

Prompt example:

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

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

Prompt example:

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

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

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

```
// Use uploadEndTimeInSeconds as current time, uploadStartTimeInSeconds as 7 days ago
const now = Math.floor(Date.now() / 1000);
const sevenDaysAgo = now - (7 * 24 * 60 * 60);
const apiUrl = `...?uploadStartTimeInSeconds=${sevenDaysAgo}&uploadEndTimeInSeconds=${now}`;
```

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

---

Source: https://www.rapidevelopers.com/bolt-ai-integrations/garmin-connect
© RapidDev — https://www.rapidevelopers.com/bolt-ai-integrations/garmin-connect
