Why Is My Fitness API Returning 401 Unauthorized?
Updated July 9, 2026
Your fitness API call is coming back 401 Unauthorized, which means the server rejected your credential, not your permissions — the access token is missing, malformed, expired, or revoked. The fix for the most common case is simple: your access token expired, so refresh it and retry. Before you do anything, though, confirm you are actually looking at a 401 and not a 403, because they have completely different fixes.
401 vs 403: read this before you touch anything
These two get confused constantly, and treating one like the other will waste your afternoon.
401 Unauthorizedmeans "who are you? your credential is bad." Authentication failed or was not supplied. The token is missing, malformed, expired, revoked, or the wrong type. Per RFC 6750, a spec-compliant server maps this to the Bearer errorinvalid_token.403 Forbiddenmeans "we know who you are, but you may not do this." The token is authentic and active, but it lacks the required scope, or your app/user is not approved for that data. This maps to the Bearer errorinsufficient_scope.
The load-bearing consequence: refreshing your token will NOT fix a 403. A refresh mints a new token with the same scopes the user already granted, so a scope problem survives the refresh untouched. If you are getting 403, stop refreshing and re-authorize the user with the missing scope instead. The rest of this guide is about 401 only.
A well-behaved server also tells you exactly what is wrong in a WWW-Authenticate response header:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="...", error="invalid_token",
error_description="The access token expired"
Read that header (and the JSON body) first — providers put the specific reason in error_description.
Reproduce it with curl and read the response
Make the failing call by hand so you can see the raw status line and headers. The -i flag prints the response headers, which is where the diagnosis lives:
curl -i "https://api.example-fitness.com/v1/user/-/activities/date/2026-07-08" \
-H "Authorization: Bearer $ACCESS_TOKEN"
Then read it top to bottom:
- Status line — confirm it says
401, not403. If it is403, go re-authorize with the right scope, not refresh. WWW-Authenticateheader — look forerror="invalid_token"(a401signal) versuserror="insufficient_scope"(a403signal).- Response body — each provider names the cause differently. Fitbit returns an
errors[]array witherrorTypevalues likeexpired_tokenorinvalid_token. Strava returns{"message":"Authorization Error","errors":[{"field":"access_token","code":"invalid"}]}. Garmin returns body text like"OAuthToken is invalid". Oura and WHOOP return401on a bad or expired token.
Most likely causes, ranked
In rough order of how often they bite, from most common to least:
- Expired access token. Access tokens are short-lived by design (often an hour to a few hours; some providers longer — verify against the provider's docs). This is the number-one cause of a sudden
401on a call that worked earlier. The fix is to refresh, not to re-authorize the user. - Missing or malformed
Authorizationheader. The header must be exactlyAuthorization: Bearer <token>. Classic breakers: noBearerprefix, a lowercasebeareron a strict server, leading/trailing whitespace or a stray newline in the token, sending the refresh token where the access token belongs, or double-encoding the token. - Revoked token. The user disconnected your app, changed their password, or you called the revoke endpoint. A refresh will also fail here — the user must re-authorize from scratch.
- Wrong token or wrong grant. A token minted for a different app or environment (staging token against prod), or an ID token used where an access token is expected. The token is valid somewhere, just not here.
How to fix it
Step 1 — Confirm it is really a 401 (not a 403)
Rerun the curl -i above and check the status line and the WWW-Authenticate error code. invalid_token (or a 401) means keep reading. insufficient_scope (or a 403) means the token is fine and you have a scope problem — re-authorize with the missing scope and stop here.
Step 2 — Check the Authorization header format
Print the exact header your client sends and eyeball it against Authorization: Bearer <access_token>. Verify: the Bearer prefix is present with correct casing, there is exactly one space, and there is no trailing newline or whitespace. Confirm you are sending the access token, not the refresh token or client secret. A quick way to catch a malformed local variable is to reproduce the exact same call in curl — if curl works and your app does not, the bug is in how your app builds the header.
Step 3 — Refresh the access token
If the header is correct and the token is simply old, exchange your stored refresh token for a new access token, then retry the original call:
curl -s -X POST "https://api.example-fitness.com/oauth/token" \
-d client_id=$CID -d client_secret=$SECRET \
-d grant_type=refresh_token \
-d refresh_token=$STORED_REFRESH_TOKEN
Persist the new access token and retry. Refresh proactively — refresh when the token has only a few minutes of life left, rather than waiting for a burst of 401s in production. Note that several providers (Strava, WHOOP, Oura, Garmin) rotate the refresh token and return a new one in this response that you must save; if your refresh "works once then fails," that is the rotation trap, covered in refresh token not working.
Step 4 — If the refresh also fails, treat the grant as dead
If the refresh call returns 400 invalid_grant (or the refresh itself 401s), the credential is gone — the user revoked access, the grant expired, or you lost the rotated refresh token. Retrying will not help. Send the user back through the full authorization flow to mint a fresh grant. See the happy-path OAuth setup in the integration guides.
Step 5 — Rule out the wrong-token and clock-skew traps
Confirm the token was issued by the same app credentials and environment you are calling (a staging token against production will 401). If token exchanges intermittently fail as "expired" the moment they are issued, check that your server clock is NTP-synced — large clock skew makes fresh tokens look already-expired. And if the token exchange itself is failing with redirect_uri errors rather than your API calls, that is a different bug: see OAuth redirect URI mismatch.
Still stuck? Quick diagnostic checklist
Run these in order:
curl -ithe failing endpoint — is it truly401, or is it403(scope problem, do not refresh)?- Read the
WWW-Authenticateheader and JSON body —invalid_tokenvsinsufficient_scope. - Diff your
Authorizationheader againstBearer <access_token>— prefix, casing, whitespace, right token. - Refresh the access token and retry the exact call.
- If refresh returns
invalid_grant, the grant is dead — re-authorize the user. - Confirm token environment matches (staging vs prod) and server clock is NTP-synced.
If all six pass and you still see 401, capture the full request and response (headers included, token redacted) and check the provider's error-handling docs for that specific errorType.
Frequently asked questions
- What is the difference between a 401 and a 403 from a fitness API?
- A 401 Unauthorized means authentication failed: the token is missing, malformed, expired, revoked, or the wrong type. A 403 Forbidden means the token is authentic but lacks the required scope or approval. The distinction matters because refreshing a token fixes many 401s but never fixes a 403 — for a 403 you must re-authorize the user with the missing scope.
- Will refreshing my token fix a 401?
- Usually yes, if the cause is an expired access token, which is the most common reason for a sudden 401. Refresh the token and retry. It will not help, though, if the token was revoked or the grant is dead — in those cases the refresh itself fails and the user must re-authorize.
- Why does my Authorization header cause a 401 even with a valid token?
- The header must be exactly the word Bearer, a space, then the access token (for example, Authorization: Bearer eyJhbGci...). Common breakers are a missing Bearer prefix, lowercase bearer on strict servers, leading or trailing whitespace or a newline in the token, sending the refresh token instead of the access token, or double-encoding the token. Reproduce the call with curl to isolate a client-side formatting bug.
- How do I know which specific cause triggered my 401?
- Read the WWW-Authenticate response header, which spec-compliant servers use to report an error like invalid_token, plus the JSON body. Providers name causes differently — Fitbit uses errorType values such as expired_token, Strava returns an Authorization Error with code invalid, and Garmin returns text like OAuthToken is invalid. Check the provider's error-handling docs for the exact string.
- My refresh worked once and now returns invalid_grant — is that a 401 problem?
- No, that is a refresh-token rotation problem. Providers such as Strava, WHOOP, Oura, and Garmin return a new refresh token on refresh and invalidate the old one immediately. If you keep reusing the original, the first refresh succeeds and later ones fail with invalid_grant. Persist the returned refresh token on every refresh.
Keep reading
Independent comparison, last reviewed July 9, 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