Skip to content
AF
Troubleshooting

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

Last verified August 12, 2026 · 7 min read

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.

Covered here:Strava

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:

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:

{
  "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 seeWhat it meansWhat to do
401 with an access_token / invalid error body, token minted hours agoOrdinary six-hour ageingRefresh, persist both tokens, retry
Refresh worked once, later refreshes return invalid_grantYou are reusing a rotated refresh tokenPersist the returned refresh_token on every refresh
Intermittent invalid_grant even though you do persist itTwo refreshes raced for the same athleteSerialize refreshes behind a per-athlete lock
Refresh returns invalid_grant and the token was definitely currentThe grant is dead — revoked or expiredClear the tokens, send the athlete back through authorize
A webhook athlete event with updates.authorized set to "false"The athlete disconnected youDelete their tokens now, do not retry
401 on the very first call after connectingHeader construction, or the wrong app's credentialsReproduce the exact call in curl before blaming Strava
429, not 401You blew the request budget, not the credentialRead the rate-limit headers and back off
Calls succeed but private activities are missingScope shortfall, not a credential problemRe-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:

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, and the happy-path setup is in the Strava API integration guide.

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.

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.

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.

Frequently asked questions

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

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 12, 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