Skip to main content
RapidDev - Software Development Agency
lovable-integrationsEdge Function Integration

How to Integrate Lovable with Garmin Connect

Integrating Garmin Connect with Lovable uses Edge Functions to handle Garmin's OAuth 1.0a authentication and proxy requests for activities, daily summaries, body composition, and sleep data. Store your Garmin API credentials in Cloud Secrets, implement HMAC-SHA1 request signing per Garmin's OAuth 1.0a requirements, and build athletic performance dashboards. Access requires applying to the Garmin Health API program. Setup takes 60-75 minutes.

What you'll learn

  • How to apply for Garmin Health API access and obtain OAuth 1.0a credentials
  • How to implement Garmin's OAuth 1.0a request signing with HMAC-SHA1 in a Deno Edge Function
  • How to receive and process Garmin's push-based activity and health data webhooks
  • How to fetch detailed performance metrics including VO2 max, training load, and HRV
  • How to build an athlete performance dashboard with Garmin data
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate13 min read70 minutesHealthMarch 2026RapidDev Engineering Team
TL;DR

Integrating Garmin Connect with Lovable uses Edge Functions to handle Garmin's OAuth 1.0a authentication and proxy requests for activities, daily summaries, body composition, and sleep data. Store your Garmin API credentials in Cloud Secrets, implement HMAC-SHA1 request signing per Garmin's OAuth 1.0a requirements, and build athletic performance dashboards. Access requires applying to the Garmin Health API program. Setup takes 60-75 minutes.

Why integrate Garmin Connect with Lovable?

Garmin is the dominant brand among serious athletes and outdoor enthusiasts, with GPS watches used by marathon runners, triathletes, cyclists, and hikers. Garmin Connect's Health API provides the richest sports performance dataset of any wearable API: GPS activity files with second-by-second heart rate, power, and pace data, daily training load and recovery metrics, VO2 max estimates and fitness age scores, HRV status and stress tracking, sleep stage analysis, and body battery energy levels.

This depth of data is what differentiates Garmin from Fitbit for sports applications. A Fitbit integration tells you how many steps someone took. A Garmin integration tells you their running efficiency, training stress score, time in each heart rate zone, and whether they're at risk of overtraining. For coaches, performance analysts, sports training platforms, and serious athletes tracking their own progress, Garmin's data is unmatched among consumer wearables.

Garmin's API differs from most fitness APIs in one important way: it's push-based rather than pull-based. Instead of your app requesting data from Garmin, Garmin sends data to your webhook endpoint when activities are recorded or daily summaries are generated. This architecture means you need a webhook receiver Edge Function running at a publicly accessible URL where Garmin can push updates. Your app then reads data from your own Supabase database rather than querying Garmin directly on demand.

Integration method

Edge Function Integration

Garmin has no native Lovable connector. Integration requires Supabase Edge Functions to handle Garmin's OAuth 1.0a authentication with HMAC-SHA1 request signing, implement push-based data delivery via webhooks (Garmin pushes data to your endpoint rather than you polling), and store received activity data in Supabase. Garmin Health API access requires applying to their developer program. All webhook processing and data storage runs through Edge Functions.

Prerequisites

  • A Lovable project with Cloud enabled and a publicly accessible URL (required for Garmin webhooks)
  • A Garmin developer account — register at developer.garmin.com
  • Garmin Health API access (requires applying at developer.garmin.com/gc-developer-program/overview/ — may take several days for approval)
  • OAuth 1.0a consumer key and secret from Garmin after approval
  • Understanding that Garmin uses a push model — Garmin sends data to you, not the reverse

Step-by-step guide

1

Apply for Garmin Health API access

Garmin's Health API is not self-serve — it requires applying to their developer program. Go to developer.garmin.com/gc-developer-program/overview/ and click 'Get Started'. You'll complete a form describing your application, intended use case (coaching platform, research, personal health app, etc.), and your organization. Garmin reviews applications and typically responds within 3-5 business days. Once approved, Garmin provides you with OAuth 1.0a credentials: a Consumer Key and Consumer Secret. These are different from OAuth 2.0 credentials — there's no client_id/client_secret separation, and the authentication flow uses HMAC-SHA1 signing rather than Bearer tokens. Garmin offers two API tiers: the standard Health API for end-user applications where users connect their Garmin accounts, and the Activity and Wellness API for personal developer use or research. For a coaching platform or multi-user app, you need the standard API with OAuth 1.0a user authorization. For accessing only your own Garmin data, direct API access is available. During the application, you must provide a webhook callback URL where Garmin will push data. Since you're building on Lovable, use your Edge Function's URL: https://your-project.supabase.co/functions/v1/garmin-webhook. Note that the Lovable preview URL won't work for Garmin webhooks — you need the deployed Supabase Edge Function URL from the Cloud panel.

Pro tip: Garmin's developer program is stricter than most fitness APIs — they require a real use case and may reject purely experimental applications. Frame your application around a specific use case with clear benefit to athletes. Personal developer access for your own Garmin data is easier to get approved than a commercial multi-user platform.

Expected result: Your Garmin developer application is submitted. You have a Consumer Key and Consumer Secret once approved. The Edge Function webhook URL is registered with Garmin.

2

Store Garmin credentials in Cloud Secrets

Open your Lovable project, click '+', select 'Cloud', and expand Secrets. Add GARMIN_CONSUMER_KEY with your OAuth 1.0a Consumer Key and GARMIN_CONSUMER_SECRET with your Consumer Secret. Also add GARMIN_API_BASE with 'https://healthapi.garmin.com/wellness-api/rest' for the Health API base URL. Garmin's OAuth 1.0a authentication requires these credentials for every request signature. Unlike OAuth 2.0 where you exchange them once for a Bearer token, OAuth 1.0a requires signing each API request with both the Consumer Secret and the user's OAuth Token Secret. Store user tokens in a Supabase garmin_tokens table: user_id, oauth_token, oauth_token_secret, garmin_user_id. Garmin's push model means most data arrives via webhooks rather than direct API pulls. However, for fetching historical data (backfilling past activities for a new connection) and fetching specific data types on demand, you'll still make outbound API requests. These use the user's OAuth token alongside your Consumer Key. The webhook receiver doesn't require the consumer credentials for verifying incoming data — Garmin sends data without signature verification in the initial implementation, though more recent versions support signature headers. Validate incoming webhook data by checking for expected fields rather than cryptographic signature verification.

Pro tip: OAuth 1.0a request signing for Garmin follows the same pattern as FatSecret (HMAC-SHA1) but uses both the Consumer Secret AND the OAuth Token Secret concatenated with '&' as the signing key: consumer_secret + '&' + token_secret. When making app-level requests (no user token), use just consumer_secret + '&' (empty token secret).

Expected result: GARMIN_CONSUMER_KEY, GARMIN_CONSUMER_SECRET, and GARMIN_API_BASE appear in Cloud Secrets. The garmin_tokens Supabase table is created.

3

Create the Garmin webhook receiver Edge Function

Garmin's push model requires a webhook receiver — an Edge Function that listens for POST requests from Garmin containing activity summaries, daily summaries, heart rate data, body composition updates, and sleep records. Garmin calls your registered webhook URL whenever new data is available for any connected user. The webhook payloads are JSON arrays containing multiple data records. An activity summary includes: summaryId, userId, activityType, startTimeInSeconds, durationInSeconds, distanceInMeters, avgHeartRateInBeatsPerMinute, maxHeartRateInBeatsPerMinute, totalKilocalories, trainingEffectLabel, and more depending on device capabilities. For daily summaries, Garmin sends: totalSteps, dailyMovementGoal, floorsClimbed, moderateIntensityDurationInSeconds, vigorousIntensityDurationInSeconds, wellnessStartTimeInSeconds, wellnessEndTimeInSeconds, and body battery data when available. The Edge Function should: validate the incoming payload has expected fields, map Garmin's user_id to your app's user_id (using a mapping table), upsert the data into your Supabase tables (activities, daily_summaries, sleep_summaries), and return a 200 OK response quickly. Process data asynchronously if the payload is large — Garmin may retry if your endpoint doesn't respond within a few seconds.

Lovable Prompt

Create a Supabase Edge Function at supabase/functions/garmin-webhook/index.ts. Accept POST requests from Garmin with no Authorization header required. Parse the JSON body which may contain arrays: activitySummaries, dailySummaries, sleepSummaries, bodyComps. For each array, map Garmin's userId to your app's user_id using a garmin_user_mapping table. Upsert activity summaries to an activities table (summary_id, user_id, activity_type, start_time, duration_seconds, distance_meters, avg_hr, max_hr, calories, training_effect) and daily summaries to daily_summaries table. Return 200 OK.

Paste this in Lovable chat

supabase/functions/garmin-webhook/index.ts
1// supabase/functions/garmin-webhook/index.ts
2import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
3import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
4
5const corsHeaders = { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "POST, OPTIONS", "Access-Control-Allow-Headers": "Content-Type" };
6
7serve(async (req) => {
8 if (req.method === "OPTIONS") return new Response(null, { headers: corsHeaders });
9
10 const supabase = createClient(Deno.env.get("SUPABASE_URL") ?? "", Deno.env.get("SUPABASE_SERVICE_ROLE_KEY") ?? "");
11
12 try {
13 const body = await req.json();
14
15 // Map Garmin user IDs to app user IDs
16 const getUserId = async (garminUserId: string): Promise<string | null> => {
17 const { data } = await supabase.from("garmin_user_mapping").select("user_id").eq("garmin_user_id", garminUserId).single();
18 return data?.user_id ?? null;
19 };
20
21 if (body.activitySummaries) {
22 for (const a of body.activitySummaries) {
23 const userId = await getUserId(a.userId);
24 if (!userId) continue;
25 await supabase.from("activities").upsert({
26 summary_id: a.summaryId,
27 user_id: userId,
28 activity_type: a.activityType,
29 start_time: new Date(a.startTimeInSeconds * 1000).toISOString(),
30 duration_seconds: a.durationInSeconds,
31 distance_meters: a.distanceInMeters ?? 0,
32 avg_hr: a.avgHeartRateInBeatsPerMinute ?? null,
33 max_hr: a.maxHeartRateInBeatsPerMinute ?? null,
34 calories: a.totalKilocalories ?? null,
35 training_effect: a.trainingEffectLabel ?? "",
36 }, { onConflict: "summary_id" });
37 }
38 }
39
40 if (body.dailySummaries) {
41 for (const d of body.dailySummaries) {
42 const userId = await getUserId(d.userId);
43 if (!userId) continue;
44 await supabase.from("daily_summaries").upsert({
45 summary_id: d.summaryId,
46 user_id: userId,
47 date: new Date(d.calendarDate).toISOString().split("T")[0],
48 steps: d.totalSteps ?? 0,
49 step_goal: d.dailyMovementGoal ?? 0,
50 floors: d.floorsClimbed ?? 0,
51 moderate_intensity_minutes: Math.round((d.moderateIntensityDurationInSeconds ?? 0) / 60),
52 vigorous_intensity_minutes: Math.round((d.vigorousIntensityDurationInSeconds ?? 0) / 60),
53 }, { onConflict: "summary_id" });
54 }
55 }
56
57 return new Response(JSON.stringify({ received: true }), { headers: { ...corsHeaders, "Content-Type": "application/json" } });
58 } catch (err) {
59 console.error("Garmin webhook error:", err.message);
60 return new Response(JSON.stringify({ error: err.message }), { status: 500, headers: corsHeaders });
61 }
62});

Pro tip: Garmin sends webhook data in batches — a single webhook call may contain multiple activity summaries for multiple users. Process all items in the batch before returning the 200 response. If processing takes too long (over 10 seconds), return 200 immediately and queue the data for async processing using a Supabase queue table.

Expected result: The garmin-webhook Edge Function is deployed at a public URL. When registered with Garmin, activity and daily summary data appears in your Supabase tables whenever connected users record activities.

4

Build the athlete performance dashboard

With data arriving via webhooks and stored in Supabase, build the performance dashboard. Because all data comes from Supabase (not direct Garmin API calls), the dashboard loads fast and works independently of Garmin's API availability. For coaches managing multiple athletes, the main view should show all athletes with their last activity date, current VO2 max (stored from webhook data), weekly training load, and recovery status (if available from body battery or HRV data). A quick visual indicator (green/yellow/red) shows who is ready to train and who may need rest. For individual athlete detail, show a 12-week training load trend, recent activities with type, distance, and key metrics, heart rate zone distribution for the current training block, and sleep quality trend if sleep data is being received. For activity-level detail, show the key metrics for each activity: duration, distance, average and max heart rate, average pace, calories, training effect label (base, aerobic, anaerobic, etc.), and training load score. Build a workout type distribution chart showing how time was split between run, bike, swim, and other activities. For RapidDev users building coaching platforms with complex multi-athlete views and automated training prescription features, RapidDev's team can help architect the data pipeline and dashboard.

Lovable Prompt

Build an athlete performance dashboard. Main view: show all users in garmin_user_mapping as athlete cards with their latest daily summary data (steps, last activity date) and latest activity (type, distance, date). Clicking an athlete shows their detail page: 8-week training volume bar chart from activities table (sum distance by week), last 10 activities list with type/distance/duration, heart rate zone distribution from last 4 weeks (use avg_hr to estimate zone from max HR). Query all data from Supabase activities and daily_summaries tables.

Paste this in Lovable chat

Pro tip: Garmin sends VO2 max estimates in the userMetrics webhook type (separate from activity and daily summaries). Create a user_metrics Supabase table and handle userMetrics in your webhook receiver to capture VO2 max and fitness age data alongside activity data.

Expected result: The performance dashboard shows athlete cards with recent activity data from Supabase. Clicking an athlete shows their training volume chart and activity history.

Common use cases

Athlete performance tracking dashboard

A coaching platform automatically receives Garmin activity data when athletes complete workouts. The webhook receiver Edge Function processes incoming activity summaries and stores training load, heart rate zones, and GPS distance data in Supabase. The coach dashboard shows all athletes' weekly training loads, VO2 max trends, and training stress scores in one view.

Lovable Prompt

Build an athlete performance dashboard. Receive Garmin activity data via webhook Edge Function and store in a Supabase activities table with: athlete_id, activity_type, date, distance, duration, avg_heart_rate, max_heart_rate, training_effect, vo2max, training_load. Display for each athlete: weekly training volume (distance + time), 4-week training load trend chart, current VO2 max estimate, and recovery status. Use data from Supabase, not direct Garmin API calls.

Copy this prompt to try it in Lovable

Running pace and heart rate zone analysis

A running coach tool analyzes the distribution of training intensity across heart rate zones from Garmin activities. When a run is received via webhook, the Edge Function stores zone distribution data (time in each HR zone). The analysis page shows zone distribution per workout and a rolling 4-week distribution helping coaches ensure athletes are following polarized training principles.

Lovable Prompt

Build a heart rate zone analyzer for runners. From Garmin webhook data, store each activity's time in 5 heart rate zones (zone1_minutes through zone5_minutes) in the activities table. Create an analysis page showing: time in zone distribution for the last 10 runs as a stacked bar chart, average zone distribution over the past 4 weeks as a pie chart, and a table of recent runs with pace, HR zones, and training effect score.

Copy this prompt to try it in Lovable

Sleep and recovery monitoring tool

A holistic wellness app tracks athlete recovery by combining Garmin's nightly sleep data, HRV status, and body battery levels with training load. Garmin pushes daily summary data including sleep scores and HRV readings. The dashboard visualizes the relationship between training load and recovery, helping athletes identify when they need extra rest.

Lovable Prompt

Build a recovery tracking dashboard using Garmin data. The Garmin webhook receives daily summaries including sleep score, HRV status, body battery start/end, and stress score. Store in a daily_summaries table. Display: 7-day sleep score trend, HRV baseline and current reading, body battery morning level over 30 days, correlation view showing training load vs. next-day HRV.

Copy this prompt to try it in Lovable

Troubleshooting

Garmin webhook sends data but it never appears in Supabase tables

Cause: The webhook receiver Edge Function is returning 200 but failing silently during data processing, or the Garmin user ID isn't found in the garmin_user_mapping table so all records are skipped.

Solution: Add detailed logging to your webhook receiver: console.log the incoming payload structure and each stage of processing. Check Cloud Logs immediately after recording a Garmin activity. Verify the garmin_user_mapping table has entries linking Garmin user IDs to your app's user IDs — without this mapping, all incoming data is skipped.

typescript
1// Log at each step for debugging
2console.log('Webhook received:', JSON.stringify(body).substring(0, 200));
3console.log('Activity count:', body.activitySummaries?.length ?? 0);

OAuth 1.0a signed requests to Garmin API return 401

Cause: Garmin's OAuth 1.0a signing requires both Consumer Secret AND OAuth Token Secret in the signing key. A common mistake is using only the Consumer Secret (correct for app-level auth) when making user-level requests that require the user's token secret as well.

Solution: For requests on behalf of a specific user, the signing key must be: CONSUMER_SECRET + '&' + OAUTH_TOKEN_SECRET. Retrieve the user's oauth_token_secret from the garmin_tokens table and include it in the signing key for all user-specific API calls. For requests not on behalf of a specific user, use: CONSUMER_SECRET + '&' (empty token secret).

typescript
1const signingKey = `${CONSUMER_SECRET}&${oauthTokenSecret ?? ''}`;

Garmin never sends webhook data after registration

Cause: Garmin's webhook registration requires the URL to be publicly accessible and respond to a validation GET request before Garmin will start pushing data. If the Edge Function URL isn't reachable or doesn't handle GET requests, Garmin may not activate the webhook.

Solution: Ensure your Edge Function handles both GET and POST methods. GET requests from Garmin's validation may include a challenge parameter that must be echoed back. Test the webhook URL is publicly accessible using a tool like reqbin.com. Check the Garmin developer console for webhook registration status and any error messages.

typescript
1if (req.method === "GET") {
2 const url = new URL(req.url);
3 const challenge = url.searchParams.get("challenge");
4 return new Response(JSON.stringify({ challenge }), { headers: { "Content-Type": "application/json" } });
5}

Best practices

  • Register your Edge Function URL with Garmin as the webhook endpoint during development — Garmin sends data to this URL, so it must be the deployed Edge Function URL, not a Lovable preview URL.
  • Create a garmin_user_mapping table that links Garmin user IDs to your Supabase user IDs immediately when users authorize your app — webhook data arrives with Garmin user IDs only.
  • Process large webhook batches asynchronously: acknowledge the webhook with 200 immediately, then process the data using a background queue to prevent Garmin from timing out and retrying.
  • Garmin's timestamps use Unix epoch seconds, not milliseconds — always multiply by 1000 when converting to JavaScript Date objects or ISO strings.
  • Cache activity aggregations (weekly volume, monthly totals) in separate Supabase tables rather than computing them from raw activity data on every dashboard load — aggregated data is much faster to query.
  • Store raw Garmin webhook payloads in a raw_webhooks table for 30 days before processing — this gives you a replay buffer if your processing logic has bugs and data needs to be re-processed.
  • Apply RLS policies to your activities and daily_summaries tables so coaches can only see data for athletes who have granted them explicit access — athlete health data requires strict privacy controls.

Alternatives

Frequently asked questions

Why does Garmin use a push model instead of a pull API like Fitbit?

Garmin's push model is designed for the typical Garmin use case: a user goes for a run, syncs their watch when they return, and the data is immediately pushed to all connected apps. This is more efficient than apps polling Garmin repeatedly waiting for new data. The tradeoff is that your app needs a publicly accessible webhook endpoint rather than simply making GET requests when the user loads your dashboard.

Can I backfill historical Garmin data for a new connection?

Yes — Garmin provides a backfill API that lets you request historical data for a specified date range when a user first connects. Call GET /wellness-api/rest/backfill/activities with start and end timestamps and Garmin will push the historical data to your webhook endpoint. This is useful for populating charts on first connection rather than starting from an empty history.

What's the difference between Garmin Health API tiers?

Garmin offers multiple API tiers: Daily Summary API (steps, calories, activity minutes), Activity Details API (GPS tracks, lap data, interval data), Wellness Summary API (sleep, stress, HRV, body battery), Body Composition API (weight, body fat, muscle mass from Garmin Index scales), and Device Information API. Access to each tier depends on your developer agreement. Start with the Daily Summary and Activity Details tiers — these cover most use cases.

How do I handle Garmin users who don't sync their watch regularly?

Garmin devices typically sync when connected to the Garmin Connect mobile app via Bluetooth. If a user doesn't sync for several days, Garmin may send multiple days of data in a single webhook batch when they finally sync. Your webhook receiver should handle batches with multiple days of daily summaries by processing each record with its specific date, rather than assuming all data is for 'today'.

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.