Skip to content
AF
Troubleshooting

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

Last verified August 11, 2026 · 5 min read

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.

Covered here:Fitbit

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:

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:

{
  "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 seeWhat it meansWhat to do
401 with errorType: expired_tokenOrdinary token ageingRefresh, persist the new token, retry
401 with errorType: invalid_token, and the header is cleanRevoked, or wrong app's tokenTry one refresh; if it fails, re-authorize
Refresh returns invalid_grantThe grant itself is goneSend the user through the authorize flow again
401 on the very first call after connectingHeader construction, or the wrong app's credentialsReproduce the exact call in curl before blaming Fitbit
403, not 401Missing scope or unapproved dataDo 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:

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; if your refresh is the thing that keeps failing, 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. 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.

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.

Frequently asked questions

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

Keep reading

Elsewhere on the site

Pages that share this one’s concepts and sources, from other sections.

Next steps

Was this page useful?

Independent comparison, last reviewed August 11, 2026. Pricing, rate limits, and feature availability change often — confirm current details in each provider’s official documentation before you commit. Product and company names are trademarks of their respective owners; AIFitnessAPI is not affiliated with, endorsed by, or sponsored by any product listed here.

← All troubleshooting · by AIFitnessAPI