---
title: "Why Is My Fitness API Returning 401 Unauthorized?"
canonical: "https://aifitnessapi.com/fix/fitness-api-401-unauthorized"
cluster: "Troubleshooting"
primary_query: "fitness api 401 unauthorized"
last_reviewed: "2026-07-09"
description: "A 401 from any fitness API means the credential was rejected: expired, malformed, or revoked. The ranked causes and fixes, provider by provider."
publisher: "AIFitnessAPI — independent, not sponsored"
cite_as: "\"Why Is My Fitness API Returning 401 Unauthorized?\", AIFitnessAPI, https://aifitnessapi.com/fix/fitness-api-401-unauthorized"
---

# Why Is My Fitness API Returning 401 Unauthorized?

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

- Canonical: https://aifitnessapi.com/fix/fitness-api-401-unauthorized
- Last reviewed: 2026-07-09
- Publisher: AIFitnessAPI (https://aifitnessapi.com) — independent, not sponsored
- Cite as: "Why Is My Fitness API Returning 401 Unauthorized?", AIFitnessAPI, https://aifitnessapi.com/fix/fitness-api-401-unauthorized

---

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

```bash
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:

```bash
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 `401`s 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](/fix/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 `401`s), 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](/integrate).

### 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](/fix/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`.

## FAQ

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

[Permalink](https://aifitnessapi.com/fix/fitness-api-401-unauthorized#faq-1)

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

[Permalink](https://aifitnessapi.com/fix/fitness-api-401-unauthorized#faq-2)

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

[Permalink](https://aifitnessapi.com/fix/fitness-api-401-unauthorized#faq-3)

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

[Permalink](https://aifitnessapi.com/fix/fitness-api-401-unauthorized#faq-4)

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

[Permalink](https://aifitnessapi.com/fix/fitness-api-401-unauthorized#faq-5)
