---
title: "Fitbit Error Code 401: What It Means and How to Fix It"
canonical: "https://aifitnessapi.com/fix/fitbit-error-code-401"
cluster: "Troubleshooting"
primary_query: "fitbit error code 401"
last_reviewed: "2026-08-11"
description: "A Fitbit 401 rejects your credential, not your permissions. Read errorType in the errors array, refresh the 8-hour access token, and know when it is a 403."
publisher: "AIFitnessAPI — independent, not sponsored"
cite_as: "\"Fitbit Error Code 401: What It Means and How to Fix It\", AIFitnessAPI, https://aifitnessapi.com/fix/fitbit-error-code-401"
---

# Fitbit Error Code 401: What It Means and How to Fix It

> Error code 401 from the Fitbit API means Fitbit rejected your credential, not your permissions: the access token is missing, malformed, expired, or no longer recognised. The usual cause is simple ageing, because a Fitbit token response carries expires_in of 28800 seconds (eight hours, verify against current docs), so refresh the token and retry. Read the errorType field inside the errors array in the response body to tell the cases apart: expired_token means refresh, while invalid_token points at a malformed header, a revoked grant, or a token minted by a different registered app. If the refresh itself fails with invalid_grant, the grant is dead and the user must authorize again.

- Canonical: https://aifitnessapi.com/fix/fitbit-error-code-401
- Last reviewed: 2026-08-11
- Publisher: AIFitnessAPI (https://aifitnessapi.com) — independent, not sponsored
- Cite as: "Fitbit Error Code 401: What It Means and How to Fix It", AIFitnessAPI, https://aifitnessapi.com/fix/fitbit-error-code-401

---

You made a Fitbit call that worked an hour ago, and now the same code, the same user, and the same endpoint come back rejected:

```bash
curl -i "https://api.fitbit.com/1/user/-/activities/date/2026-08-10.json" \
  -H "Authorization: Bearer $ACCESS_TOKEN"
```

Before you touch your OAuth code, check the clock. A Fitbit token response carries `"expires_in": 28800` — 28,800 seconds, or eight hours (the commonly documented lifetime; verify the current value in Fitbit's docs). If the token you are sending was minted this morning and it is now this evening, you have found your bug and the fix is a refresh, not a re-auth.

## Read the body, not just the status line

Fitbit does not make you guess. A rejected call returns an `errors[]` array, and the field that decides what you do next is `errorType`. Reduced to the part you act on:

```json
{
  "errors": [
    { "errorType": "expired_token" }
  ]
}
```

Two values matter here:

- **`expired_token`** — the credential was valid and aged out. Refresh it.
- **`invalid_token`** — the credential is not one Fitbit recognises as live: malformed, revoked, or issued by a different app. A refresh may or may not save you; see the decision path below.

Log `errorType` on every 401 rather than logging "401" and moving on. It is the difference between a one-line retry and an afternoon.

## Ranked causes of a Fitbit 401

In rough order of how often they actually bite:

1. **The eight-hour access token expired.** This is the overwhelming majority of Fitbit 401s, and it is the one that shows up as "it worked this morning." Refresh with the stored `refresh_token` and retry the original call.
2. **A malformed or missing `Authorization` header.** The header must be exactly the word `Bearer`, one space, then the access token. The classic breakers are a missing `Bearer ` prefix, stray whitespace or a trailing newline inside the token string, and sending the refresh token where the access token belongs.
3. **The user revoked consent.** Someone disconnected your app or reset their credentials. The tell is that the *refresh* fails too, with `invalid_grant` — at which point the grant is dead and no amount of retrying resurrects it. The user has to re-authorize from scratch.
4. **A token minted by a different registered app.** Every Fitbit app registration gets its own Client ID, and a token issued under one is not valid for another. Teams that keep a **Personal** app for intraday testing alongside a **Server** app for production hit this constantly: the token is genuinely valid, just not here.

## The decision path

Work it top to bottom and stop at the first branch that fires.

| What you see | What it means | What to do |
| --- | --- | --- |
| `401` with `errorType: expired_token` | Ordinary token ageing | Refresh, persist the new token, retry |
| `401` with `errorType: invalid_token`, and the header is clean | Revoked, or wrong app's token | Try one refresh; if it fails, re-authorize |
| Refresh returns `invalid_grant` | The grant itself is gone | Send the user through the authorize flow again |
| `401` on the very first call after connecting | Header construction, or the wrong app's credentials | Reproduce the exact call in curl before blaming Fitbit |
| `403`, not `401` | Missing scope or unapproved data | Do not refresh — see below |

## Refreshing correctly (public vs confidential apps)

The refresh call differs by the OAuth 2.0 application type you chose at registration, and getting it wrong produces its own failures. Public **Client** apps — mobile and single-page apps that ship no secret — send the `client_id` and rely on PKCE:

```bash
curl -X POST "https://api.fitbit.com/oauth2/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=refresh_token" \
  -d "refresh_token=REFRESH_TOKEN" \
  -d "client_id=CLIENT_ID"
```

Confidential **Server** apps add HTTP Basic authentication with their client credentials on top of the same request:

```
Authorization: Basic BASE64(client_id:client_secret)
```

Persist whatever comes back, and refresh *before* expiry rather than waiting for production to hand you a burst of 401s at the eight-hour mark. The full registration and token-exchange walkthrough lives in the [Fitbit API integration guide](/integrate/fitbit-api); if your refresh is the thing that keeps failing, [refresh token not working](/fix/refresh-token-not-working) covers the rotation traps that make a refresh succeed once and never again.

## When it is really a 403

A `403 Forbidden` is a different animal with a different fix: the credential is fine, but it does not carry the permission for what you asked. Refreshing mints a new token with the *same* scopes the user already granted, so a scope gap survives the refresh untouched.

It bites on Fitbit specifically because consent is per data collection: the user chooses which collections to grant, and Fitbit does not let you force them to grant every scope. Someone can approve `activity` and decline `heartrate`, and your heart-rate calls will keep failing no matter how many times you refresh. Read the `scope` value returned with the token instead of assuming you got what you asked for, and re-authorize with the missing collection when you did not.

## The general case, and the migration in the background

Everything above is the Fitbit-flavoured version of a failure mode every OAuth fitness provider shares. For the cross-provider version — the `WWW-Authenticate` header, how Strava, Garmin, Oura, and WHOOP each name the same conditions, and the generic 401-versus-403 triage — see [fitness API 401 unauthorized](/fix/fitness-api-401-unauthorized). If your 401s arrive in bursts rather than one at a time, check whether you are actually looking at throttling: [Fitbit API 429 rate limit](/fix/fitbit-api-429-rate-limit).

One piece of context worth holding while you debug: Google is retiring the legacy Fitbit Web API in favour of the Google Health API, targeted for around September 2026 with the exact day still to be confirmed — verify it against the current docs. Existing Fitbit access and refresh tokens do not transfer, and every user must re-consent through Google OAuth 2.0. That is not the cause of today's 401, but it does mean the auth code you are fixing has a shelf life. The re-consent path is mapped out in [migrating from the Fitbit Web API to Google Health](/migrate/fitbit-web-api-to-google-health).

## FAQ

### How long does a Fitbit access token last before it expires?

A Fitbit token response includes expires_in with a value of 28800 seconds, which is eight hours. That is the commonly documented lifetime, so verify the current value against Fitbit's docs before hard-coding it. Practically, it means a long-running integration must refresh at least three times a day, and it explains why a call that worked this morning fails this evening.

[Permalink](https://aifitnessapi.com/fix/fitbit-error-code-401#faq-1)

### Does refreshing a Fitbit token require HTTP Basic auth?

It depends on the OAuth 2.0 application type you registered. Public Client apps, such as mobile and single-page apps that ship no secret, send grant_type, the refresh token, and the client_id, relying on PKCE. Confidential Server apps additionally send an HTTP Basic authorization header built from the client ID and client secret. Sending the wrong combination for your app type makes the refresh fail even though the refresh token is good.

[Permalink](https://aifitnessapi.com/fix/fitbit-error-code-401#faq-2)

### What does Fitbit put in the response body when it rejects a call?

Fitbit returns an errors array, and the field to read is errorType. The two values you will see most on a rejected credential are expired_token, meaning the access token aged out and a refresh will fix it, and invalid_token, meaning Fitbit does not recognise the credential as live. Log errorType on every failure rather than logging the status code alone.

[Permalink](https://aifitnessapi.com/fix/fitbit-error-code-401#faq-3)

### Why does a Fitbit token that works in one of my apps fail in another?

Every Fitbit app registration gets its own Client ID, and a token minted under one registration is not valid for another. The token is genuinely live, just not for the app making the call. This bites teams that keep a Personal app for intraday testing next to a Server app for production and let the two credentials mix in a shared config or environment file.

[Permalink](https://aifitnessapi.com/fix/fitbit-error-code-401#faq-4)

### Does a Fitbit 401 mean the user disconnected my app?

Sometimes, and the refresh call is what tells you. If the refresh succeeds, the original failure was ordinary token ageing and the connection is intact. If the refresh fails with invalid_grant, the grant itself is gone because the user revoked access or it expired, and no retry will bring it back. At that point the only path forward is sending the user through the authorization flow again.

[Permalink](https://aifitnessapi.com/fix/fitbit-error-code-401#faq-5)
