# Garmin Connect

- Tool: FlutterFlow
- Difficulty: Advanced
- Time required: 4-6 hours (plus partner approval wait time)
- Last updated: July 2026

## TL;DR

Connecting FlutterFlow to Garmin Connect requires a Garmin Health API partner-program approval (no self-serve key exists) and a Firebase Cloud Function proxy to perform OAuth 1.0a HMAC-SHA1 request signing — impossible to do securely in client Dart. Your FlutterFlow API Group points at the proxy, which signs and forwards requests to healthapi.garmin.com/wellness-api/rest, and a separate Cloud Function receives Garmin's push Pings into Firestore.

## Two Things That Make Garmin Connect Harder Than Other Fitness APIs

There are two facts about Garmin Connect that most tutorial pages gloss over, and they significantly shape your integration architecture. First: Garmin Health API access is gated behind a formal partner-program application at developer.garmin.com/health-api. You cannot generate a key and start calling the API today — you must apply, describe your use case, and wait for Garmin to approve your request. There is no sandbox you can test with during the wait. This gate exists because Garmin's users' health data is sensitive, and Garmin vets who it shares it with.

Second: once approved, the Garmin Health API authenticates with OAuth 1.0a — not the modern OAuth 2.0 Bearer tokens used by Fitbit or Google. OAuth 1.0a requires computing a per-request HMAC-SHA1 signature using your consumer secret and the user's token secret. This signature must be computed on the server because computing it in Dart client code would expose the consumer secret in the app binary, and doing it correctly in Dart for every request edge case is genuinely difficult. The practical requirement is a Firebase Cloud Function (or Supabase Edge Function) that holds your secrets, computes the signature, and proxies requests.

Garmin's API is also primarily push-based. Rather than polling for new data, Garmin sends Ping webhook notifications to a URL you register when a user's device syncs. Your Cloud Function must be the receiver for these Pings, process the data, and write it to Firestore — where your FlutterFlow app can read it in real time. For approved partners building serious health apps, this architecture is robust. For a quick prototype, the approval gate alone means this is not a weekend project.

## Before you start

- Approved Garmin Health API partner credentials (consumer key and consumer secret) — apply at developer.garmin.com/health-api
- Firebase project with Cloud Functions enabled (Blaze pay-as-you-go plan required for outbound HTTP in Cloud Functions)
- FlutterFlow project on a paid plan with Custom Code and API Calls access
- Node.js knowledge for writing the Firebase Cloud Function proxy and webhook receiver
- Familiarity with OAuth 1.0a concepts (consumer key/secret, request token, access token/secret, HMAC-SHA1 signature)

## Step-by-step guide

### 1. Apply to the Garmin Health API partner program

The very first step is not code — it is an application. Go to developer.garmin.com/health-api and find the partner program application form. You will need to describe your use case, your company or project, and the types of Garmin data you intend to access (daily summaries, activities, sleep, heart rate, etc.). Garmin reviews each application manually because their partner API provides access to personal health data. Approval timelines vary — plan for anywhere from one week to several weeks. While you wait, you can build the rest of your FlutterFlow app, design the UI, set up Firestore, and write your Cloud Functions. Once approved, Garmin will issue you a consumer key and consumer secret. Store these immediately in Firebase environment configuration or Secret Manager — never in code. Also during this period, review the Garmin Health API documentation (available to approved partners) to understand the exact endpoints and JSON schemas for the data types you want. The partner portal also provides information about the push Ping/Push model: Garmin sends data to your registered webhook URL rather than waiting for you to poll. Register your Cloud Function URL as the Ping receiver during onboarding. Keep your consumer key and consumer secret secure — these credentials identify your application and grant access to all approved users' health data.

**Expected result:** You receive a partner approval email from Garmin with your consumer key and consumer secret, and your webhook URL is registered in the Garmin partner portal.

### 2. Deploy a Cloud Function proxy for OAuth 1.0a signing

The OAuth 1.0a signing proxy is the technical heart of this integration. Create a new Firebase Cloud Function (or a Supabase Edge Function if you prefer Deno) that accepts requests from your FlutterFlow app and forwards them to the Garmin Health API with a correctly computed OAuth 1.0a signature. The function needs to: read the Garmin base URL and endpoint path from the request, compute the OAuth 1.0a authorization header using the consumer key, consumer secret, user's access token, and user's token secret (which are stored in Firestore after the user connects their Garmin account), and make the signed request to `https://healthapi.garmin.com/wellness-api/rest` on behalf of the client. Store the consumer key and consumer secret in Firebase environment configuration using `firebase functions:config:set garmin.consumer_key=... garmin.consumer_secret=...` or in Secret Manager. The user's per-user access token and token secret are stored in Firestore after the OAuth authorization flow (step 3) and retrieved by user ID when signing. The proxy should return the Garmin response body as-is to your FlutterFlow app so you can parse it with JSON paths. Deploy the function to your Firebase project with `firebase deploy --only functions`. Note the deployed HTTPS URL — this is the base URL you will use in your FlutterFlow API Group.

```
// index.js (Firebase Cloud Function — Garmin OAuth 1.0a signing proxy)
const functions = require('firebase-functions');
const admin = require('firebase-admin');
const axios = require('axios');
const OAuth = require('oauth-1.0a');
const crypto = require('crypto');

admin.initializeApp();

exports.garminProxy = functions.https.onRequest(async (req, res) => {
  const userId = req.query.userId;
  if (!userId) return res.status(400).json({ error: 'userId required' });

  // Load user token from Firestore
  const userDoc = await admin.firestore().collection('garmin_tokens').doc(userId).get();
  if (!userDoc.exists) return res.status(401).json({ error: 'No Garmin token for user' });

  const { accessToken, tokenSecret } = userDoc.data();
  const consumerKey = functions.config().garmin.consumer_key;
  const consumerSecret = functions.config().garmin.consumer_secret;

  const oauth = OAuth({
    consumer: { key: consumerKey, secret: consumerSecret },
    signature_method: 'HMAC-SHA1',
    hash_function: (base, key) =>
      crypto.createHmac('sha1', key).update(base).digest('base64'),
  });

  const endpoint = req.query.endpoint || '/dailies';
  const garminUrl = `https://healthapi.garmin.com/wellness-api/rest${endpoint}`;
  const requestData = { url: garminUrl, method: 'GET' };
  const token = { key: accessToken, secret: tokenSecret };

  const authHeader = oauth.toHeader(oauth.authorize(requestData, token));

  const response = await axios.get(garminUrl, {
    headers: { ...authHeader, 'Accept': 'application/json' },
    params: req.query,
  });

  res.json(response.data);
});
```

**Expected result:** The Cloud Function is deployed and accessible at its HTTPS URL. Calling it with a valid userId query parameter returns Garmin API data for that user.

### 3. Run the OAuth user authorization flow and store tokens

Before you can fetch data for a specific Garmin user, that user must authorize your application through the OAuth 1.0a flow. This involves three steps: first, your Cloud Function calls Garmin's request-token endpoint to get a temporary request token. Second, your app redirects the user to Garmin's authorization URL where the user logs into their Garmin Connect account and approves your application. Third, Garmin redirects back to your callback URL with a verifier, and your Cloud Function exchanges the request token + verifier for a permanent access token and token secret, which you save to Firestore under the user's ID. In FlutterFlow, implement the redirect by building a WebView Custom Widget or using the `url_launcher` package in a Custom Action to open the Garmin authorization URL in a browser. Build a Cloud Function endpoint that handles the OAuth callback redirect, completes the token exchange, and writes `{ accessToken, tokenSecret }` to Firestore for that user. Once stored in Firestore, the proxy Cloud Function (from step 2) retrieves these tokens to sign every subsequent API request. After the user connects their Garmin account, the FlutterFlow app can call the proxy endpoint to fetch their data. Show a 'Connect Garmin Account' button in your UI that launches this flow, and update the UI to a connected state once tokens are stored in Firestore.

**Expected result:** After a user completes the Garmin authorization flow, their access token and token secret are written to Firestore and visible in the Firebase console under the garmin_tokens collection.

### 4. Create the FlutterFlow API Group pointing at your proxy

In your FlutterFlow project, click API Calls in the left navigation panel, then click + Add → Create API Group. Name it 'Garmin' and set the Base URL to your deployed Cloud Function HTTPS URL (for example, `https://us-central1-your-project.cloudfunctions.net/garminProxy`). Do not point it at healthapi.garmin.com directly — that bypasses the proxy and exposes your consumer secret. Under the API Group's Headers, you do not need to add any authentication headers — the proxy handles all OAuth 1.0a signing. Now add individual API Calls within the group. Click + Add Call → Create API Call. Name it 'GetDailySummary', set the method to GET, and set the endpoint to leave as empty (the proxy uses the `endpoint` query parameter to determine which Garmin endpoint to call). Add a Variable in the Variables tab named `userId` (String) and another named `endpoint` (String, default value `/dailies`). These become query parameters in the request URL automatically. In the API Call's Response & Test section, paste a sample Garmin daily summary JSON and click Generate JSON Paths to create extractors for the fields you need (steps, calories, stress, etc.). Repeat this for other Garmin endpoints you want to expose (sleep: `/sleep`, heart rate: `/heartRateZones`, activities: `/activityDetails/{activityId}`). Click Save and test each call using the Test Request button with a real userId and endpoint value.

```
// Sample API Call configuration
{
  "name": "GetDailySummary",
  "method": "GET",
  "base_url": "https://us-central1-YOUR_PROJECT.cloudfunctions.net/garminProxy",
  "endpoint": "",
  "query_params": {
    "userId": "{{ userId }}",
    "endpoint": "/dailies",
    "startTimeInSeconds": "{{ startTime }}",
    "endTimeInSeconds": "{{ endTime }}"
  },
  "headers": {}
}
```

**Expected result:** The Garmin API Group appears in FlutterFlow's API Calls panel with configured calls. Test requests using a real userId return Garmin data and generate JSON paths successfully.

### 5. Add a webhook receiver for Garmin Ping notifications and bind data to the UI

Garmin's primary data delivery model is push, not pull. When a user's Garmin device syncs, Garmin sends a Ping notification to the webhook URL you registered with the partner program. This Ping tells you that new data is available for a given user and data type. Your webhook receiver Cloud Function must be a publicly accessible HTTPS endpoint registered with Garmin. Create a second Cloud Function called `garminWebhook` that handles POST requests from Garmin. When a Ping arrives, parse the notification body to identify the userId and data type (daily summary, activity, sleep, etc.), then call the appropriate Garmin API endpoint (via your signing proxy logic) to fetch the actual data, and write it to Firestore. In FlutterFlow, set up a Firestore listener (use the Backend Query → Firestore Collection or Document on your page and enable Real Time) to read the latest wellness data for the logged-in user. This way, when Garmin sends a Ping and the Cloud Function writes to Firestore, your FlutterFlow app updates its UI in real time without the user having to manually refresh. Bind the Firestore data to your chart widgets, step count Text widgets, and sleep summary cards. For immediate data on page load, also trigger the GetDailySummary API Call directly (polling the proxy) as a fallback alongside the Firestore listener.

```
// garminWebhook Cloud Function (receives Garmin Pings)
exports.garminWebhook = functions.https.onRequest(async (req, res) => {
  // Garmin sends a JSON body with userId and data type info
  const body = req.body;
  const userId = body.userId;
  const dataType = body.dataType; // e.g. 'dailies', 'activities', 'sleep'

  // Acknowledge quickly — Garmin retries on slow responses
  res.status(200).send('OK');

  // Fetch the actual data from Garmin using the signing proxy logic
  // (reuse signing utility from the proxy function)
  // Then write to Firestore:
  await admin.firestore()
    .collection('garmin_data')
    .doc(userId)
    .set({ [dataType]: body, updatedAt: admin.firestore.FieldValue.serverTimestamp() },
         { merge: true });
});
```

**Expected result:** Garmin Pings are received by the webhook Cloud Function, data is written to Firestore, and the FlutterFlow app's real-time listener updates the UI automatically when new wellness data arrives.

## Best practices

- Never place the Garmin consumer secret or user token secrets in FlutterFlow API Call headers or App Constants — they are client-side and extractable from the compiled app
- Always return HTTP 200 to Garmin's Ping webhook immediately, then process the data asynchronously — slow responses cause Garmin to retry and create duplicate writes
- Store per-user Garmin access tokens in Firestore with the user's Firebase Auth UID as the document ID, secured by Firestore rules so users cannot read each other's tokens
- Implement both a Firestore real-time listener (for Ping-pushed data) and a fallback polling API Call (for the current day's data on page load) so users always see fresh data
- Register only the Garmin data type Pings you actually need in the partner portal — receiving Pings for unused data types wastes Cloud Function invocations and Firestore writes
- Cache Garmin daily summaries in Firestore rather than re-querying the Garmin API on every app open — the partner program has rate limits, and Firestore reads are cheaper and faster
- Document your OAuth 1.0a proxy Cloud Function with clear comments about where each secret comes from and never commit real credentials to a public GitHub repository

## Use cases

### Garmin-connected athlete performance dashboard

Build a coaching app that displays daily wellness summaries (steps, stress, body battery, sleep scores) from Garmin Connect. When a user's Garmin device syncs, the Cloud Function webhook receiver writes the new data to Firestore and the FlutterFlow app updates its dashboard in real time via a Firestore listener.

Prompt example:

```
A dashboard page that shows today's Garmin daily summary: steps, calories, stress level, body battery, and sleep score, with a 7-day trend chart for each metric
```

### Corporate wellness tracker

Create a corporate wellness app where employees connect their Garmin devices, and a team-level dashboard shows aggregate step counts and active minutes across the organization. Individual data is stored in Firestore with row-level security; team averages are computed by a Cloud Function and exposed as a summary endpoint.

Prompt example:

```
A team wellness leaderboard showing each employee's weekly step total from Garmin, with a company-wide average at the top
```

### Sleep quality and recovery app

Build a recovery-focused app that pulls Garmin sleep data (sleep stages, HRV, sleep score) via the Health API's sleep endpoint. Display a nightly sleep score card and a week-over-week comparison chart to help users understand recovery trends from their Garmin wearable.

Prompt example:

```
A recovery page showing last night's Garmin sleep score, REM/deep/light sleep breakdown in a pie chart, and a 14-day sleep score trend line
```

## Troubleshooting

### 401 Unauthorized from the Garmin Health API — signature verification failed

Cause: The OAuth 1.0a HMAC-SHA1 signature is incorrect. Common causes: wrong consumer secret or token secret, incorrect signature base string construction, timestamp clock skew, or a URL encoding issue in the normalized parameters.

Solution: Use the `oauth-1.0a` npm package to handle the signature computation rather than implementing HMAC-SHA1 manually. Verify that the consumer secret and token secret stored in Firebase config match the values Garmin issued. Check that your server's system clock is accurate (OAuth 1.0a is timestamp-sensitive). Log the full signature base string and compare it to Garmin's debugging tools if available in the partner portal.

### No Garmin data appears in the app even after the user connects their account

Cause: Garmin uses a push model — data does not appear until Garmin sends a Ping webhook after the user's device syncs. If no Ping has arrived yet, the Firestore document for that user is empty, and the FlutterFlow UI has nothing to display.

Solution: Implement a fallback polling call on the GetDailySummary API endpoint for the current day, triggered On Page Load, so users see data even before a Ping arrives. Also confirm that your webhook URL is correctly registered in the Garmin partner portal, and test by having the user sync their Garmin device (open Garmin Connect app → pull to refresh).

### XMLHttpRequest error or CORS error when testing in FlutterFlow's web preview

Cause: Your FlutterFlow API Group is pointing directly at healthapi.garmin.com (or the browser is blocked from reaching your Cloud Function). Web builds enforce CORS; native mobile builds do not. If you incorrectly pointed the API Group at Garmin directly, the browser's CORS policy blocks the request.

Solution: Confirm your API Group Base URL is your Cloud Function URL, not healthapi.garmin.com. Ensure your Cloud Function adds CORS headers (`Access-Control-Allow-Origin: *` or the specific FlutterFlow origin) in its response. For native iOS/Android builds, CORS is not enforced and direct calls would work, but the OAuth secret exposure risk still requires the proxy regardless.

### Partner application rejected or no response from Garmin

Cause: Garmin's partner review process is manual and selective. Incomplete applications, vague use-case descriptions, or use cases outside Garmin's partner program terms can result in rejection or delay.

Solution: Review Garmin's partner program guidelines at developer.garmin.com/health-api and ensure your application describes a clear, specific use case with named data types. If rejected, you may reapply with a more detailed description or contact Garmin developer relations. In the meantime, consider whether Fitbit's self-serve OAuth 2.0 API (fitbit-api) or on-device data via the HealthKit / Health Connect Custom Action could serve your use case instead.

## Frequently asked questions

### Can I test the Garmin Health API without being an approved partner?

No. There is no sandbox key or free developer tier for the Garmin Health API. Access requires formal partner-program approval from Garmin, and there is no way to generate a consumer key independently. During the approval wait, you can build your Cloud Function and FlutterFlow UI using mock Garmin JSON data, but live device data requires approved credentials.

### Why can't I put the OAuth 1.0a signing in the FlutterFlow Custom Action instead of a Cloud Function?

OAuth 1.0a requires your consumer secret and the user's token secret to compute the HMAC-SHA1 signature. If those secrets are in Dart code, they ship inside the compiled Flutter app binary and can be extracted by anyone who decompiles the APK or IPA. The Cloud Function proxy keeps secrets server-side where they are never exposed to end users. This is non-negotiable for Garmin partner compliance.

### Do I need to poll Garmin for new data, or does Garmin push it to me?

Garmin's primary model is push, not pull. When a user's Garmin device syncs with the Garmin Connect app, Garmin sends a Ping webhook notification to the URL you registered as the partner. Your Cloud Function must be the receiver for these Pings. However, you should also implement a fallback polling call for the current day's data on page load, because Pings are only sent after a device sync — users who haven't synced recently won't have data pushed yet.

### Does this integration work for web builds of my FlutterFlow app?

The FlutterFlow API Group calling your Cloud Function proxy works on all platforms including web. The proxy removes the CORS issue that would occur if you called healthapi.garmin.com directly from a browser. However, Garmin Connect data is inherently tied to physical wearable devices — users will need the Garmin Connect mobile app to sync their device, so the web version of your app is typically a companion dashboard rather than the primary sync interface.

### If this integration is so complex, is there a simpler alternative?

If you need cross-platform wearable data without partner approval, Fitbit API uses self-serve OAuth 2.0 and is significantly simpler to set up. If you only need iOS health data, HealthKit via the `health` Custom Action requires no backend at all. If you are building a serious health app specifically for Garmin users and need the full wellness dataset, the Garmin partner program complexity is unavoidable — RapidDev's team builds FlutterFlow integrations like this every week and can scope the architecture on a free call at rapidevelopers.com/contact.

---

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