Skip to content
AFAIFitnessAPI
Troubleshooting

Why Is My Fitness API Returning 401 Unauthorized?

Updated July 9, 2026

A 401 Unauthorized from a fitness API means the server rejected your credential, not your permissions: the access token is missing, malformed, expired, or revoked. The most common cause is an expired access token, so refresh it and retry. Do not confuse 401 with 403 (Forbidden) — a 403 means the token is valid but lacks the required scope, and refreshing will not fix it; you must re-authorize with the missing scope instead.

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 Unauthorized means "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 error invalid_token.
  • 403 Forbidden means "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 error insufficient_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, not 403. If it is 403, go re-authorize with the right scope, not refresh.
  • WWW-Authenticate header — look for error="invalid_token" (a 401 signal) versus error="insufficient_scope" (a 403 signal).
  • Response body — each provider names the cause differently. Fitbit returns an errors[] array with errorType values like expired_token or invalid_token. Strava returns {"message":"Authorization Error","errors":[{"field":"access_token","code":"invalid"}]}. Garmin returns body text like "OAuthToken is invalid". Oura and WHOOP return 401 on a bad or expired token.

Most likely causes, ranked

In rough order of how often they bite, from most common to least:

  1. 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 401 on a call that worked earlier. The fix is to refresh, not to re-authorize the user.
  2. Missing or malformed Authorization header. The header must be exactly Authorization: Bearer <token>. Classic breakers: no Bearer prefix, a lowercase bearer on 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.
  3. 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.
  4. 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:

  1. curl -i the failing endpoint — is it truly 401, or is it 403 (scope problem, do not refresh)?
  2. Read the WWW-Authenticate header and JSON body — invalid_token vs insufficient_scope.
  3. Diff your Authorization header against Bearer <access_token> — prefix, casing, whitespace, right token.
  4. Refresh the access token and retry the exact call.
  5. If refresh returns invalid_grant, the grant is dead — re-authorize the user.
  6. 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