---
title: "Strava API 401 Unauthorized: What It Means and How to Fix It"
canonical: "https://aifitnessapi.com/fix/strava-api-401-unauthorized"
cluster: "Troubleshooting"
primary_query: "strava api 401 unauthorized"
last_reviewed: "2026-08-12"
description: "A Strava 401 rejects your credential. Read the Authorization Error body, check the six-hour expires_at, and persist the rotating refresh token every time."
publisher: "AIFitnessAPI — independent, not sponsored"
cite_as: "\"Strava API 401 Unauthorized: What It Means and How to Fix It\", AIFitnessAPI, https://aifitnessapi.com/fix/strava-api-401-unauthorized"
---

# Strava API 401 Unauthorized: What It Means and How to Fix It

> A 401 from the Strava API means Strava rejected the credential itself, not your permissions, and on Strava the top-ranked cause is the rotating refresh token rather than the access token. Strava returns a new refresh token on every refresh and the old one stops working, so an integration that persists only the access token refreshes once and then can never mint another, and every subsequent call fails. Read the response body first, because Strava reports a rejected credential as an Authorization Error naming the access_token field with code invalid. Then check expires_at, since access tokens expire roughly six hours after creation (an expires_in of 21600 seconds as of 2026 — verify against the current docs). If the refresh call itself returns invalid_grant, the grant is dead and the athlete has to authorize again.

- Canonical: https://aifitnessapi.com/fix/strava-api-401-unauthorized
- Last reviewed: 2026-08-12
- Publisher: AIFitnessAPI (https://aifitnessapi.com) — independent, not sponsored
- Cite as: "Strava API 401 Unauthorized: What It Means and How to Fix It", AIFitnessAPI, https://aifitnessapi.com/fix/strava-api-401-unauthorized

---

There is a version of this bug that looks like an outage and is not one. Every Strava call for every athlete starts failing at once, your error rate goes vertical, and Strava's status page says everything is fine. It usually is fine. What happened is that your refresh loop ran, Strava handed back a new refresh token, your code kept the old one, and from that moment on nothing could mint a working access token for anybody.

That is the shape of a Strava 401. Start by making the failing call by hand so you can see the raw response:

```bash
curl -i "https://www.strava.com/api/v3/athlete/activities?per_page=1" \
  -H "Authorization: Bearer $ACCESS_TOKEN"
```

## Read the body Strava sends back

Strava does not leave the reason in the status line alone. A rejected credential comes back in a documented shape:

```json
{
  "message": "Authorization Error",
  "errors": [
    { "field": "access_token", "code": "invalid" }
  ]
}
```

The load-bearing part is `"field": "access_token"`. Strava is telling you the credential you presented is the thing it refused — not the endpoint, not the athlete, not the scope. Log that body on every failure. A team that logs only the status code spends an afternoon on a problem the body names in one line.

## Ranked causes, Strava-specific

The general cross-provider ranking puts an expired access token first. On Strava the order is different, because Strava rotates refresh tokens and that rotation is the single most common way an integration breaks.

1. **You did not persist the rotated refresh token.** Strava's own documentation says it plainly: *"the refresh token may or may not be the same refresh token used to make the request. Applications should persist the refresh token contained in the response and always use the most recent refresh token."* Once a new one is issued, the old one is dead. Symptom: refresh succeeds exactly once, then returns `invalid_grant`, and every API call afterwards is a 401 because you can no longer produce a live access token.
2. **The access token simply expired.** Strava access tokens expire about six hours after creation — an `expires_in` of 21600 seconds as of 2026, so verify the current value. Every token response also carries `expires_at`, which is the field to store and compare against. Symptom: it worked this morning, it does not work this evening, and a single refresh fixes it.
3. **A malformed header, or the wrong token in it.** The header must be exactly the word `Bearer`, one space, then the access token. The classic breakers are a missing prefix, a trailing newline inside the token string, and sending the refresh token or the client secret where the access token belongs.
4. **The athlete deauthorized your app.** Strava gives you a signal for this before your calls fail. The Events API sends an `athlete` update carrying `updates.authorized` set to `"false"` when someone disconnects you, and that is your cue to clean up their stored tokens. If you are not consuming that event, the first you hear about it is a refresh that returns `invalid_grant`.
5. **A token from the wrong application or environment.** A credential minted by a different registration, or a staging token pointed at production, is genuinely valid somewhere — just not here.
6. **Clock skew.** If freshly issued tokens look already expired, your server clock is far enough off that expiry maths goes wrong. Keep servers NTP-synced before you touch the OAuth code.

## The decision path

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

| What you see | What it means | What to do |
| --- | --- | --- |
| 401 with an `access_token` / `invalid` error body, token minted hours ago | Ordinary six-hour ageing | Refresh, persist both tokens, retry |
| Refresh worked once, later refreshes return `invalid_grant` | You are reusing a rotated refresh token | Persist the returned `refresh_token` on every refresh |
| Intermittent `invalid_grant` even though you do persist it | Two refreshes raced for the same athlete | Serialize refreshes behind a per-athlete lock |
| Refresh returns `invalid_grant` and the token was definitely current | The grant is dead — revoked or expired | Clear the tokens, send the athlete back through authorize |
| A webhook `athlete` event with `updates.authorized` set to `"false"` | The athlete disconnected you | Delete their tokens now, do not retry |
| 401 on the very first call after connecting | Header construction, or the wrong app's credentials | Reproduce the exact call in curl before blaming Strava |
| 429, not 401 | You blew the request budget, not the credential | Read the rate-limit headers and back off |
| Calls succeed but private activities are missing | Scope shortfall, not a credential problem | Re-authorize with `activity:read_all` |

## Fixing the rotation properly

The refresh call itself is unremarkable, and the token exchange and refresh do not count against your API rate limit, so there is no reason to be shy about refreshing on a buffer:

```bash
curl -s -X POST "https://www.strava.com/oauth/token" \
  -d "client_id=$CID" \
  -d "client_secret=$SECRET" \
  -d "grant_type=refresh_token" \
  -d "refresh_token=$STORED_REFRESH_TOKEN"
```

What comes back is a fresh access token, a new `expires_at`, and a `refresh_token` field that may differ from the one you sent. That last field is the whole ballgame. Write both tokens back in a single atomic update, overwriting the stored refresh token even when the value looks identical, and store `expires_at` alongside them. If your code persists the access token and the refresh-token write is conditional, skipped, or lost to a crash between two statements, you have recreated the bug you came here to fix.

Two disciplines go on top of that write. Serialize refreshes per athlete, because a background job and a user-triggered refresh firing together will rotate the token out from under each other and produce `invalid_grant` on a codebase whose persistence is otherwise correct. And refresh proactively rather than waiting for a burst of rejections in production — note that Strava only mints a genuinely new access token when the current one has roughly an hour or less left, so verify that behaviour against the current docs before tuning your buffer. The cross-provider version of this failure, including the providers that rotate the same way, lives in [refresh token not working](/fix/refresh-token-not-working), and the happy-path setup is in the [Strava API integration guide](/integrate/strava-api).

## When Strava is not returning 401 at all

Two near misses are worth ruling out explicitly.

A **scope shortfall on Strava does not usually surface as an error.** Plain `activity:read` returns only activities the athlete has shared beyond "Only You", and the same filtering applies to webhook events. The athlete's private runs quietly do not appear while every call returns 200. Read the `scope` value Strava returns with the token instead of assuming you got what you asked for — athletes can deselect scopes on the consent screen — and re-authorize with `activity:read_all` if you need the full history.

A **failing token exchange is a different bug from a failing API call.** If the rejection happens when you swap the authorization code for tokens, and you are seeing `invalid_grant` or a redirect complaint rather than an `access_token` error body, the usual cause is that the `redirect_uri` does not exactly match what is registered under your Authorization Callback Domain. That triage is in [OAuth redirect URI mismatch](/fix/oauth-redirect-uri-mismatch).

## The program context, and what it does not explain

Strava's developer program has tightened since 2024: formal Standard and Extended Access tiers with Standard serving a limited number of athletes, a program review with display and branding rules covering the "Connect with Strava" button and screenshots of every surface where Strava data appears, athlete-consent requirements that must be visible in your UI, restrictions on routing data through intermediary platforms, and a reported paid Strava subscription now gating Standard tier access. Those are real constraints on whether your application keeps access, and the audit they imply is laid out in [adapting to Strava's API changes](/migrate/adapt-to-strava-api-changes).

What our sources do not establish is any link between those program rules and a 401 status code. So do not let a compliance theory displace the token diagnosis: check the body, check `expires_at`, check the rotation, and only then go read the current agreement. For the cross-provider triage — how 401 differs from 403, what the `WWW-Authenticate` header adds, and how Fitbit, Garmin, Oura and WHOOP each name the same conditions — start at [fitness API 401 unauthorized](/fix/fitness-api-401-unauthorized).

## FAQ

### What does Strava's Authorization Error response body tell me?

It tells you Strava rejected the credential rather than the request. Our corpus records the shape as a message of Authorization Error alongside an errors array whose single entry names the access_token field with the code invalid. That is a credential-level rejection, so the next question is always whether the token aged out, was never rotated correctly, or belongs to a different registered application. Log the body rather than the bare status code, because the field name is what tells you the access token specifically is the thing Strava refused.

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

### Does a missing activity:read_all scope make Strava return 401?

No, and this is the Strava-specific trap. On Strava a scope shortfall usually shows up as missing data rather than an error status. Plain activity:read only ever returns activities the athlete shared beyond Only You, and the same filtering applies to webhook events, so private runs simply vanish from your listings while every call keeps succeeding. If athletes report missing activities, check the scope value returned with the token instead of hunting for a rejected credential.

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

### Do Strava token refresh calls count against my rate limit?

No. Our Strava integration guide records that the OAuth token exchange and refresh calls do not count against your API rate limit. That matters when you are fixing a 401 storm, because the instinct to batch refreshes carefully is misplaced here — the thing you must actually protect is your per-application request budget, documented as roughly 200 requests per 15 minutes and 2,000 per day as of 2026, which returns 429 rather than 401 when you exceed it. Verify the current quotas before you build around them.

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

### How do I tell a deauthorized athlete from an expired Strava token?

The refresh call is the test. If the refresh succeeds, the original rejection was ordinary ageing and the connection is intact. If it comes back invalid_grant, the grant itself is gone and no retry recovers it. Strava also gives you an advance signal: the Events API sends an athlete update carrying updates.authorized set to false when someone disconnects your app, which is your cue to clean up that athlete's stored tokens before your next call fails.

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

### Could Strava's developer subscription requirement be causing my 401s?

Treat that as unproven rather than as a cause. Strava's developer program has tightened since 2024 with formal Standard and Extended Access tiers, display and branding rules including the Connect with Strava button, athlete-consent requirements, and a reported paid subscription now gating Standard tier access. Those are real constraints on whether your application keeps access at all, but our corpus does not document any of them producing a 401 status code specifically, so do not skip the token diagnosis in favour of a compliance theory. Verify the current terms against Strava's live developer agreement and API Policy.

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