# How to Integrate Fitbit API with V0

- Tool: V0
- Difficulty: Intermediate
- Time required: 50 minutes
- Last updated: March 2026

## TL;DR

To integrate the Fitbit API with V0 by Vercel, generate a health dashboard UI with V0, implement OAuth2 authorization code flow in Next.js API routes to authenticate users with their Fitbit accounts, fetch activity, sleep, and heart rate data server-side, and deploy on Vercel. Users log in with Fitbit and see their personal health metrics in your custom dashboard.

## Build Personal Health Dashboards with Fitbit API and V0

Fitbit tracks millions of health data points — daily steps, calories burned, sleep stages, resting heart rate, activity minutes, and more. The Fitbit Web API makes this data available to authorized applications, enabling developers to build custom health dashboards, research tools, coaching apps, and wellness platforms that display users' own Fitbit data in tailored interfaces. V0 can generate sophisticated health dashboard UIs in minutes; the integration work involves implementing Fitbit's OAuth2 flow so users can authorize your app to access their data.

Fitbit uses the standard OAuth2 authorization code flow: your app redirects users to Fitbit's authorization page, the user grants permission, Fitbit redirects back to your callback URL with an authorization code, and your API route exchanges that code for access and refresh tokens. This is more complex than API key auth but necessary because Fitbit data is personal — the user must explicitly authorize each application to access their health information. Access tokens expire after 8 hours, so your app needs to use refresh tokens to maintain access without requiring re-authorization.

The Fitbit API has a generous free tier with no cost for standard health data access. Rate limits are 150 API calls per hour per user for most endpoints. Fitbit's Intraday data (minute-by-minute resolution) requires applying for permission from Fitbit due to its granularity. For standard dashboards showing daily summaries, step counts, and sleep, the default permissions are sufficient.

## Before you start

- A Fitbit developer account — register at dev.fitbit.com and create a new application to get your client ID and secret
- Your Fitbit application's client ID and client secret from the developer portal
- The callback URL registered in your Fitbit app — must match exactly: https://your-app.vercel.app/api/fitbit/callback
- A V0 account at v0.dev to generate the health dashboard UI
- A Vercel account with your app deployed — you need a stable HTTPS URL for the OAuth callback before testing the full flow

## Step-by-step guide

### 1. Register a Fitbit App and Generate the Dashboard UI with V0

Before generating the UI, register your application at dev.fitbit.com. Create a new app, set the application type to 'Personal' for personal use or 'Server' for multi-user apps, and add your callback URL as https://your-vercel-url.vercel.app/api/fitbit/callback. You can use a placeholder URL initially and update it after your first Vercel deployment. Fitbit will give you a client ID and client secret — copy these for later. Then open V0 at v0.dev and generate your health dashboard. Describe the metrics you want to display: step counts, sleep data, heart rate, calories. V0 works well with health dashboard patterns — ring charts for goal progress, timeline charts for sleep stages, trend lines for weekly data. The key design decision is how to handle the pre-authorization state: the dashboard needs a 'Connect Fitbit Account' CTA for first-time users and the full metrics view for returning users. V0 can generate both states if you describe them. Push to GitHub using V0's Git panel.

**Expected result:** A Fitbit developer app is registered with your callback URL, and a V0-generated health dashboard renders in preview with both the pre-connection CTA state and the connected metrics view using mock data.

### 2. Implement the Fitbit OAuth2 Authorization Flow

Create two API routes to handle Fitbit's OAuth2 authorization code flow. The first route (/api/fitbit/auth) builds the Fitbit authorization URL and redirects the user to Fitbit's login page. The authorization URL requires your client ID, a redirect URI (your callback URL), response_type=code, and a scope string specifying which data you want access to. Fitbit scopes include activity, sleep, heartrate, nutrition, profile, settings, and location. Request only the scopes you actually display — users see exactly what you're requesting and fewer scopes builds more trust. The second route (/api/fitbit/callback) handles Fitbit's redirect back to your app after the user authorizes. It receives a code query parameter, exchanges it for tokens using a POST request to Fitbit's token endpoint, and stores the tokens securely. Token storage is the architectural decision point: for a single-user personal app, storing in an encrypted cookie or a session is fine. For a multi-user app, store tokens in a database keyed by user ID. The access token expires after 8 hours; the refresh token is valid indefinitely until used or revoked. Include the refresh token workflow so your app doesn't require users to re-authorize daily.

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

export async function GET() {
  const clientId = process.env.FITBIT_CLIENT_ID!;
  const redirectUri = `${process.env.NEXT_PUBLIC_APP_URL}/api/fitbit/callback`;
  const scope = 'activity sleep heartrate profile';

  const params = new URLSearchParams({
    response_type: 'code',
    client_id: clientId,
    redirect_uri: redirectUri,
    scope,
    expires_in: '604800', // 7 days
  });

  return NextResponse.redirect(
    `https://www.fitbit.com/oauth2/authorize?${params.toString()}`
  );
}

// app/api/fitbit/callback/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { cookies } from 'next/headers';

export async function GET(request: NextRequest) {
  const { searchParams } = new URL(request.url);
  const code = searchParams.get('code');
  const error = searchParams.get('error');

  if (error || !code) {
    return NextResponse.redirect(
      `${process.env.NEXT_PUBLIC_APP_URL}/?error=fitbit_auth_failed`
    );
  }

  const clientId = process.env.FITBIT_CLIENT_ID!;
  const clientSecret = process.env.FITBIT_CLIENT_SECRET!;
  const redirectUri = `${process.env.NEXT_PUBLIC_APP_URL}/api/fitbit/callback`;

  const credentials = Buffer.from(`${clientId}:${clientSecret}`).toString('base64');

  try {
    const tokenRes = await fetch('https://api.fitbit.com/oauth2/token', {
      method: 'POST',
      headers: {
        'Authorization': `Basic ${credentials}`,
        'Content-Type': 'application/x-www-form-urlencoded',
      },
      body: new URLSearchParams({
        code,
        grant_type: 'authorization_code',
        redirect_uri: redirectUri,
      }).toString(),
    });

    if (!tokenRes.ok) {
      throw new Error(`Token exchange failed: ${tokenRes.status}`);
    }

    const tokens = await tokenRes.json();

    // Store tokens in cookies (for single-user app)
    // For multi-user: store in database keyed by authenticated user ID
    const cookieStore = await cookies();
    cookieStore.set('fitbit_access_token', tokens.access_token, {
      httpOnly: true,
      secure: true,
      maxAge: tokens.expires_in,
      sameSite: 'lax',
    });
    cookieStore.set('fitbit_refresh_token', tokens.refresh_token, {
      httpOnly: true,
      secure: true,
      maxAge: 60 * 60 * 24 * 30, // 30 days
      sameSite: 'lax',
    });

    return NextResponse.redirect(`${process.env.NEXT_PUBLIC_APP_URL}/dashboard`);
  } catch (err) {
    console.error('Fitbit OAuth error:', err);
    return NextResponse.redirect(
      `${process.env.NEXT_PUBLIC_APP_URL}/?error=token_exchange_failed`
    );
  }
}
```

**Expected result:** Visiting /api/fitbit/auth redirects to Fitbit's login page. After authorizing, Fitbit redirects back to /api/fitbit/callback, which stores tokens in cookies and redirects to the dashboard.

### 3. Create the Fitbit Data API Routes with Token Refresh

Create the API routes that fetch real Fitbit data using the stored OAuth tokens. These routes read the access token from cookies, call Fitbit's API endpoints, and handle token expiry. The Fitbit Web API base URL is https://api.fitbit.com/1/user/-/. The dash (-) represents the authorized user. Key endpoints include: /activities/date/{date}.json for daily activity summary, /sleep/date/{date}.json for sleep data, /activities/heart/date/{date}/1d.json for heart rate. Create a shared getFitbitData helper that reads the token from cookies, makes the API call, and handles 401 responses by attempting a token refresh. The token refresh flow calls https://api.fitbit.com/oauth2/token with grant_type=refresh_token and the stored refresh token — using the same Basic Auth header with your client credentials. If refresh also fails (refresh token expired or revoked), clear the stored tokens and return a response indicating the user needs to re-authorize. This token refresh handling prevents users from needing to reconnect their Fitbit daily.

```
// app/api/fitbit/today/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { cookies } from 'next/headers';

async function refreshFitbitToken(refreshToken: string): Promise<string | null> {
  const credentials = Buffer.from(
    `${process.env.FITBIT_CLIENT_ID}:${process.env.FITBIT_CLIENT_SECRET}`
  ).toString('base64');

  const res = await fetch('https://api.fitbit.com/oauth2/token', {
    method: 'POST',
    headers: {
      Authorization: `Basic ${credentials}`,
      'Content-Type': 'application/x-www-form-urlencoded',
    },
    body: new URLSearchParams({
      grant_type: 'refresh_token',
      refresh_token: refreshToken,
    }).toString(),
  });

  if (!res.ok) return null;

  const data = await res.json();
  const cookieStore = await cookies();
  cookieStore.set('fitbit_access_token', data.access_token, {
    httpOnly: true, secure: true, maxAge: data.expires_in, sameSite: 'lax',
  });
  cookieStore.set('fitbit_refresh_token', data.refresh_token, {
    httpOnly: true, secure: true, maxAge: 60 * 60 * 24 * 30, sameSite: 'lax',
  });
  return data.access_token;
}

async function fitbitGet(path: string, accessToken: string): Promise<Response> {
  return fetch(`https://api.fitbit.com${path}`, {
    headers: { Authorization: `Bearer ${accessToken}` },
    cache: 'no-store',
  });
}

export async function GET(request: NextRequest) {
  const cookieStore = await cookies();
  let accessToken = cookieStore.get('fitbit_access_token')?.value;
  const refreshToken = cookieStore.get('fitbit_refresh_token')?.value;

  if (!accessToken && !refreshToken) {
    return NextResponse.json({ error: 'Not authenticated', needsAuth: true }, { status: 401 });
  }

  const today = new Date().toISOString().split('T')[0];

  // Try fetching with current token; refresh if expired
  let activityRes = accessToken
    ? await fitbitGet(`/1/user/-/activities/date/${today}.json`, accessToken)
    : null;

  if ((!activityRes || activityRes.status === 401) && refreshToken) {
    accessToken = await refreshFitbitToken(refreshToken);
    if (!accessToken) {
      return NextResponse.json({ error: 'Session expired, please reconnect Fitbit', needsAuth: true }, { status: 401 });
    }
    activityRes = await fitbitGet(`/1/user/-/activities/date/${today}.json`, accessToken);
  }

  if (!activityRes || !activityRes.ok) {
    return NextResponse.json({ error: 'Failed to fetch Fitbit data' }, { status: 500 });
  }

  const [activityData, sleepRes, heartRes] = await Promise.all([
    activityRes.json(),
    fitbitGet(`/1.2/user/-/sleep/date/${today}.json`, accessToken),
    fitbitGet(`/1/user/-/activities/heart/date/${today}/1d.json`, accessToken),
  ]);

  const sleepData = sleepRes.ok ? await sleepRes.json() : null;
  const heartData = heartRes.ok ? await heartRes.json() : null;

  const summary = activityData.summary || {};
  const mainSleep = sleepData?.sleep?.[0];

  return NextResponse.json({
    date: today,
    activity: {
      steps: summary.steps || 0,
      caloriesOut: summary.caloriesOut || 0,
      activeMinutes: (summary.veryActiveMinutes || 0) + (summary.fairlyActiveMinutes || 0),
      distance: summary.distances?.find((d: { activity: string }) => d.activity === 'total')?.distance || 0,
    },
    sleep: mainSleep ? {
      totalMinutes: mainSleep.minutesAsleep,
      efficiency: mainSleep.efficiency,
      deepMinutes: mainSleep.levels?.summary?.deep?.minutes || 0,
      remMinutes: mainSleep.levels?.summary?.rem?.minutes || 0,
      lightMinutes: mainSleep.levels?.summary?.light?.minutes || 0,
    } : null,
    heartRate: {
      restingHeartRate: heartData?.['activities-heart']?.[0]?.value?.restingHeartRate || null,
    },
  });
}
```

**Expected result:** GET /api/fitbit/today returns today's step count, calories, active minutes, sleep data, and resting heart rate for the authorized user. If the token is expired, it automatically refreshes before fetching.

### 4. Configure Fitbit Credentials in Vercel and Deploy

Push your project to GitHub, then configure credentials in Vercel. Open the Vercel Dashboard → Settings → Environment Variables. Add FITBIT_CLIENT_ID (the OAuth 2.0 Client ID from your Fitbit app's developer portal page), FITBIT_CLIENT_SECRET (the Client Secret from the same page — treat this as a password, never expose it), and NEXT_PUBLIC_APP_URL (your deployed Vercel URL, e.g., https://your-app.vercel.app — this is safe to expose since it's just your domain). The FITBIT_CLIENT_ID and FITBIT_CLIENT_SECRET must not have NEXT_PUBLIC_ prefix. After deploying, return to the Fitbit Developer Portal and update your app's OAuth 2.0 Callback URL to your actual Vercel deployment URL: https://your-app.vercel.app/api/fitbit/callback. This must match exactly — including the https scheme and no trailing slash. With credentials set and the callback URL updated, test the full OAuth flow by visiting your deployed app and clicking 'Connect with Fitbit'. After authorizing, you should be redirected back to your dashboard with real Fitbit data loading. RapidDev's team can assist if you're building a multi-user coaching platform that needs a database-backed token storage system rather than the cookie approach used here.

**Expected result:** The full Fitbit OAuth flow works in your deployed Vercel app. Users click 'Connect with Fitbit', authorize the app, and return to a dashboard showing their real health metrics. Tokens refresh automatically without requiring daily re-authorization.

## Best practices

- Store Fitbit OAuth tokens in httpOnly, Secure cookies (or server-side database) — never in localStorage or regular cookies accessible to JavaScript
- Always update both access_token and refresh_token when performing a token refresh — Fitbit invalidates used refresh tokens and sends new ones
- Request only the Fitbit permission scopes your dashboard actually displays — requesting fewer scopes builds user trust and reduces authorization friction
- Use /1.2/ version of the sleep API endpoint for accurate sleep stage breakdown data (deep, REM, light) — the /1/ version returns less detailed sleep information
- Handle the case where users haven't worn their device or synced recently — show empty states rather than errors for missing data
- For multi-user apps, store tokens in a database rather than cookies, encrypted at rest, with the user's ID as the lookup key
- Test token refresh logic before launch by manually expiring tokens in your test database and verifying the dashboard still works without requiring re-authorization

## Use cases

### Personal Health Dashboard

A personal dashboard where users log in with their Fitbit account and see today's activity summary alongside historical trends. Shows step count progress toward daily goal, calories burned, active minutes, sleep duration from last night, and resting heart rate.

Prompt example:

```
Create a personal health dashboard with a 'Connect Fitbit' button at the top that links to /api/fitbit/auth. Once connected, show: a step counter card with progress bar toward 10,000 step goal, a calories burned card, an active minutes card, a sleep summary showing total sleep and sleep stages (Light/Deep/REM) from last night as a pie chart, and resting heart rate. Data from /api/fitbit/today. Use a clean health app aesthetic with green accents.
```

### 7-Day Activity Trend Report

A weekly health report that shows the user's activity data over the past 7 days with trend charts for steps, calories, and sleep. Highlights best and worst days, calculates weekly averages, and shows progress toward weekly activity goals.

Prompt example:

```
Build a weekly health report with a bar chart of daily steps over 7 days (highlight the best day in green), a line chart of sleep hours per night, a calories burned trend, and weekly average cards for steps/sleep/calories. Include a 'streak' indicator showing consecutive active days. Fetch data from /api/fitbit/weekly. Modern minimal health tracker design.
```

### Coaching Platform Integration

A wellness coaching platform where clients connect their Fitbit accounts and coaches see their activity data in a multi-client dashboard. Each client's daily step count, sleep quality score, and activity level is displayed alongside coaching notes.

Prompt example:

```
Design a coaching dashboard showing a list of connected clients with their latest Fitbit metrics: today's steps vs goal, last night's sleep score (0-100), heart rate zone minutes, and a colored status indicator (green/yellow/red based on goal completion). Clicking a client shows their detailed weekly trends. Data from /api/fitbit/client/{id}. Clean table-based layout with expandable rows.
```

## Troubleshooting

### OAuth callback returns 'redirect_uri_mismatch' error from Fitbit

Cause: The redirect URI in your auth request doesn't exactly match the Callback URL registered in the Fitbit Developer Portal. Fitbit is strict about exact URL matching — even a trailing slash difference causes this error.

Solution: Go to dev.fitbit.com → your app → OAuth 2.0 Application Type. Copy the exact URL from your NEXT_PUBLIC_APP_URL/api/fitbit/callback and paste it as the Callback URL. Include https://, no trailing slash. Ensure NEXT_PUBLIC_APP_URL in Vercel matches your actual deployment URL exactly.

### Fitbit API returns 401 after token refresh — user has to reconnect repeatedly

Cause: Fitbit's refresh tokens are single-use — once you use a refresh token to get a new access token, Fitbit also sends a new refresh token. If you don't store the new refresh token, the next refresh attempt fails because the old token is invalidated.

Solution: Always update both the access_token and refresh_token cookies/database records when a token refresh succeeds. The refreshFitbitToken function must store data.refresh_token (the new one) along with data.access_token.

```
// In refreshFitbitToken, store BOTH tokens:
cookieStore.set('fitbit_access_token', data.access_token, { ... });
cookieStore.set('fitbit_refresh_token', data.refresh_token, { ... }); // NEW refresh token
```

### Sleep data returns null or empty sleep array for some users

Cause: The user hasn't worn their Fitbit device during sleep, hasn't synced their device, or the sleep date is today before the user has gone to sleep. Fitbit only records sleep when the device detects sleep patterns.

Solution: Handle null sleep data gracefully in your UI with a message like 'No sleep data recorded for this date'. Check yesterday's date (not today) for sleep data when showing morning dashboards — Fitbit's sleep record for a night usually falls under the date the user woke up.

```
// Use yesterday's date for sleep (more likely to have data than today):
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
const sleepDate = yesterday.toISOString().split('T')[0];
```

### Cookies are not being set in the callback route after token exchange

Cause: In Next.js App Router, the cookies() function from 'next/headers' requires being called in a Server Component or Route Handler context. If the callback is redirecting via NextResponse.redirect() immediately after setting cookies, the cookie storage might not be committed before the redirect.

Solution: Ensure you're using await cookies() from 'next/headers' (not the request cookies object). Create the NextResponse object for the redirect, set cookies on it directly using response.cookies.set(), and then return the response.

```
// Set cookies on the redirect response directly:
const response = NextResponse.redirect(`${process.env.NEXT_PUBLIC_APP_URL}/dashboard`);
response.cookies.set('fitbit_access_token', tokens.access_token, { httpOnly: true, secure: true });
response.cookies.set('fitbit_refresh_token', tokens.refresh_token, { httpOnly: true, secure: true });
return response;
```

## Frequently asked questions

### Does the Fitbit API require users to have a paid Fitbit Premium subscription?

No. The Fitbit API is available to all Fitbit device users regardless of Premium subscription status. Premium subscribers have access to additional app features and insights within Fitbit's own apps, but the API endpoints for activity, sleep, and heart rate data work for all device owners. Some intraday (minute-by-minute) data endpoints require a separate API access application to Fitbit.

### Can I access heart rate zone data from the Fitbit API?

Yes. The /activities/heart/date/{date}/1d.json endpoint returns heart rate zone data including minutes spent in Fat Burn, Cardio, and Peak zones as well as the resting heart rate. Heart rate data requires the 'heartrate' scope to be requested in your OAuth authorization. Continuous heart rate tracking requires a Fitbit device that supports heart rate monitoring (most modern Fitbit devices do).

### What is the Fitbit API rate limit?

The Fitbit API allows 150 API calls per user per hour for most endpoints. This is per-user, not per-application, so a coaching platform with 1,000 users effectively has 150,000 calls per hour total (150 per user). Dashboard calls that fetch multiple data types (activity + sleep + heart rate) in parallel count as 3 separate calls. With typical dashboard usage patterns, most apps stay well under this limit.

### How do I get access to Fitbit's Intraday (minute-by-minute) data?

Fitbit requires a separate API application to access Intraday data — you must submit a request through the Fitbit developer portal explaining your use case. Intraday data provides minute-by-minute step counts, heart rate, and activity data, which is useful for detailed athletic analysis. Standard daily summary data doesn't require this application process.

### Will this Fitbit integration still work after Google acquired Fitbit?

The Fitbit Web API continues to function as of 2026, though Google has been integrating Fitbit data more deeply with Google Health. Fitbit's developer platform at dev.fitbit.com remains active and the OAuth2 API described in this guide is the current standard. Monitor Fitbit's developer blog for any deprecation announcements as Google continues integration work.

### Can I display Fitbit data alongside sleep data from Apple Health or Google Fit?

Yes, but each platform has its own separate OAuth flow and API. Apple HealthKit requires iOS-native code (not available in a Next.js web app), while Google Fit has its own REST API with Google OAuth2. For a cross-platform health dashboard, you'd implement separate OAuth flows for each platform and merge the data server-side in your API routes.

---

Source: https://www.rapidevelopers.com/v0-integrations/fitbit-api
© RapidDev — https://www.rapidevelopers.com/v0-integrations/fitbit-api
