Skip to content
AF
Concepts

What Are OAuth Scopes (and How Health APIs Grant Them)?

Last verified August 12, 2026 · 7 min read

An OAuth scope is a named string attached to an access token that caps what that token may read or write — Fitbit's activity or heartrate, Strava's activity:read_all, WHOOP's read:sleep. In health APIs the user grants them per collection rather than all-or-nothing, so the set you requested and the set you received routinely differ and you have to read the granted scope back out of the response. A scope is also only one layer of permission: an OS permission like HealthKit or Health Connect is a device access control, and a platform entitlement like Apple's HealthKit capability or Garmin's partner-level program grant is a build- or business-level gate, and each fails differently. The signature worth memorising is that a missing scope is a 403 with insufficient_scope rather than a 401, and refreshing never fixes it, because a refresh mints a token carrying the same scopes the user already granted.

Covered here:FitbitStravaHealthKitWHOOPGarminHealth Connect

The set you requested is not the set you received#

The single most consequential thing about health-API scopes is that the user picks them individually, and most builders write code as though the request were the grant.

Fitbit is explicit about it: the platform does not let you force a user to grant every scope, because consent is per data collection. Someone can approve activity and decline heartrate on the same screen. Fitbit's documented collections include activity, heartrate, sleep and profile, with the wider set covering location, nutrition, oxygen_saturation, respiratory_rate, settings, social, temperature and weight — verify the current list before you build a picker around it.

Strava behaves the same way and says so in the redirect: the athlete can deselect scopes on the consent screen, so a granted set narrower than the requested set is normal rather than an error. Oura documents eight scopes — email, personal, daily, heartrate, workout, tag, session and spo2 — and the user can toggle individual ones at consent too.

The design consequence is not subtle. If a missing collection throws your onboarding into a generic failure state, you have converted a partial grant into a lost user. Degrade per feature instead: hide the sleep card, keep the activity card, and tell the person exactly which toggle to flip if they want the rest.

Read the grant, don't infer it#

Every provider hands the granted scope back; the annoyance is that they hand it back in different places and different formats.

ProviderWhere the granted scope appearsFormat gotcha
FitbitA scope field in the token-exchange response, next to access_token, refresh_token, expires_in and user_idSpace-delimited
StravaThe scope query parameter on the redirect back to your callback, and in the exchangeComma-delimited, unlike most OAuth providers
WHOOPThe scopes you requested at authorize time, including the non-data one covered belowSpace-delimited in the authorize URL

A parser written against one delimiter and reused for the next provider is a genuinely common bug, and it fails in the worst possible direction: your code decides a scope is absent, disables a working feature, and nobody files a ticket because the app looks intentional. Store the granted set per connection at exchange time, compare it against what each feature needs, and re-check it after any re-authorization. The step-by-step version of that exchange lives in the Fitbit integration guide.

Scope, permission, entitlement: three different words that fail three different ways#

"Permission" gets used for all of these in conversation, which is how teams end up debugging the wrong layer. They are separate mechanisms with separate grantors.

LayerWho grants itWhere it bitesTypical failure
OAuth scopeThe end user, on the provider's consent screenThe provider's API, per request403 with insufficient_scope on an otherwise valid token
OS permissionThe end user, in a system sheet, per data typeThe deviceHealthKit tells you nothing about a denied read — it looks identical to an empty store. Health Connect needs a manifest declaration, a runtime grant, and separate permissions again for background reads and for history
Platform entitlement / capabilityApple or Google, to your signed buildBuild and runtime, before any user is involvedWithout the HealthKit capability, HKHealthStore calls fail outright; without the background-delivery entitlement, enableBackgroundDelivery fails with errorAuthorizationDenied
Partner or program approvalThe vendor, to your organisationBefore you hold credentials at allGarmin's Connect Developer Program is partner-approval-only, and which scopes even exist for you depends on which programs your partnership covers. Fitbit gates minute-level intraday data by app type and separate approval
Store declarationYou declare, a reviewer acceptsReleaseA Play Console health-data declaration must list every type you read or write, and asking for types you cannot justify can get the release rejected

Read down that table and the diagnostic value is obvious: a 403 is a scope problem, an empty HealthKit read may be nothing at all, errorAuthorizationDenied is a signing problem, and "we never got credentials" is a business-development problem. None of them is fixed by the others' fix.

There is a fifth layer that is not technical. An OS permission grant is a device-level access control, not automatically a lawful basis for what you then do with the data, and an OAuth scope is a clean, auditable consent to access rather than consent for downstream purposes like marketing. Consent for health data covers where the legal instrument has to sit separately from the technical one.

The failure signature worth memorising: 401 is not 403#

This distinction wastes more afternoons than any other part of the topic.

  • 401 Unauthorized means the credential itself was rejected — missing, malformed, expired, revoked, or the wrong type. A spec-compliant server maps it to the Bearer error invalid_token.
  • 403 Forbidden means the credential is authentic and active but is not permitted to do this. That maps to insufficient_scope.

The load-bearing part: refreshing will never fix a 403, because a refresh mints a new token carrying the same scopes the user already granted. If you retry-with-refresh on a scope error you will loop forever against a provider that is answering correctly. The only fix is to send the user back through authorization asking for the missing scope. Read the WWW-Authenticate response header before deciding which situation you are in; the ranked causes on the credential side are in fitness API 401 Unauthorized.

One honest caveat: providers overload 403. On an Oura usercollection endpoint it usually means the token is missing a required scope — but a lapsed Oura membership produces the same status. So a 403 narrows the problem to "authentic token, not allowed", not specifically to "you forgot a scope".

Some scopes are not about data at all#

Two examples from the corpus are worth knowing because both present as something other than a scope bug.

WHOOP's offline scope controls whether you receive a refresh token, not what you can read. Skip it and the authorization flow succeeds, the first API calls succeed, and then background sync dies silently when the access token expires — with no refresh token to renew it and no obvious cause in your logs. It shows up in triage as a token problem, which is why it sits in refresh token not working rather than in a scope guide.

Strava's activity:read_all is usually described as the private-activity scope, and it is, but it also governs your push stream: plain activity:read only ever sees activities the athlete shared beyond "Only You", and the same restriction applies to the webhook events you receive. So a scope decision made during onboarding quietly determines what arrives on your callback endpoint months later.

Choosing a scope set you won't regret#

Least privilege is the standing rule, and it has three separate payoffs rather than one. A shorter consent screen converts better. A narrower token has a smaller blast radius if it leaks. And a request you can justify is a request that survives an app-store health-data review.

Practically: map each scope to the specific feature that needs it, and if you cannot name the feature, drop the scope. Design every feature to degrade on its own when its collection is missing. Keep your Android manifest declarations, runtime permission set and Play Console declaration in sync with each other, because they are three lists of the same thing that drift independently. And re-read the granted scope after every re-authorization, not just the first — the user who declined heart rate in January may grant it in March, and nothing will tell you unless you look.

For the surrounding flow — the authorization-code exchange, access versus refresh tokens, PKCE, and the on-device exception where none of this applies — see what OAuth is for health data.

Frequently asked questions

Can a user grant only some of the scopes my app requested?
Yes, and on health APIs you should assume they will. Fitbit documents that you cannot force a user to grant every scope because consent is per data collection, so someone can approve activity and decline heart rate. Strava athletes can deselect scopes on the consent screen, and Oura users can toggle individual scopes from its eight documented ones. Design each feature to degrade on its own when its collection is missing, rather than failing onboarding as a whole.
How do I find out which scopes were actually granted?
Read them back from the provider rather than assuming. Fitbit returns a scope field in the token-exchange response alongside the access token, refresh token, expiry and user id. Strava returns the granted scope as a query parameter on the redirect to your callback as well as in the exchange. Watch the delimiter: Fitbit uses spaces and Strava uses commas, unlike most OAuth providers, so a parser reused across providers will silently decide a granted scope is absent.
Is an Android or iOS health permission the same thing as an OAuth scope?
No. An OAuth scope is granted by the user at a cloud provider's consent screen and enforced by that provider's API. An OS permission is a device-level access control granted in a system sheet per data type, and it behaves differently: HealthKit never tells you a read was denied, and Health Connect needs a manifest declaration, a runtime grant, and separate permissions again for background reads and for history. They are also different instruments legally, since a device permission is not automatically a lawful basis for what you do with the data afterwards.
Which error code means a scope is missing?
A 403 Forbidden, mapping to the Bearer error insufficient_scope, means the token is authentic but not permitted to do that. A 401 Unauthorized, mapping to invalid_token, means the credential itself was rejected as missing, malformed, expired or revoked. Refreshing fixes many 401s and never fixes a 403, because the new token carries the same scopes. One caveat: providers overload 403, so on an Oura usercollection endpoint it can mean a missing scope or a lapsed membership.
Why did I get no refresh token even though the login worked?
Usually because you omitted the scope that unlocks offline access. WHOOP documents offline as a required scope to receive a refresh token at all, and it controls capability rather than data. Without it the authorization succeeds, your first API calls succeed, and background sync then dies silently when the access token expires with nothing to renew it. Re-run the authorization flow with the correct scope; no amount of retrying the token endpoint helps.

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 concepts · by AIFitnessAPI