---
title: "Troubleshooting"
canonical: "https://aifitnessapi.com/fix"
type: "cluster-index"
pages: "25"
last_reviewed: "2026-09-04"
publisher: "AIFitnessAPI"
---

# Troubleshooting

> 25 pages. Each entry below shows the question the page owns, followed by its answer capsule.

## Why Is My Fitness API Returning 401 Unauthorized?

- Question: fitness api 401 unauthorized
- HTML: https://aifitnessapi.com/fix/fitness-api-401-unauthorized
- Markdown: https://aifitnessapi.com/fix/fitness-api-401-unauthorized.md
- Last reviewed: 2026-07-09

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.

## How to Fix the OAuth redirect_uri Mismatch Error

- Question: oauth redirect_uri mismatch
- HTML: https://aifitnessapi.com/fix/oauth-redirect-uri-mismatch
- Markdown: https://aifitnessapi.com/fix/oauth-redirect-uri-mismatch.md
- Last reviewed: 2026-07-09

The OAuth redirect_uri_mismatch error means the redirect_uri your app sends is not byte-for-byte identical to a callback URL registered in the provider's developer console. OAuth servers do an exact string comparison, so http vs https, localhost vs 127.0.0.1, a trailing slash, a port, path case, or encoding differences all break it. Copy the registered value and the value your code actually sends, diff them character by character, and make them match. The same mismatch caught at the token step can surface as invalid_grant instead, so read the error_description.

## Why Is My Fitness API Refresh Token Not Working?

- Question: fitness api refresh token not working
- HTML: https://aifitnessapi.com/fix/refresh-token-not-working
- Markdown: https://aifitnessapi.com/fix/refresh-token-not-working.md
- Last reviewed: 2026-07-09

If your refresh works once and then every later attempt returns 400 invalid_grant, you almost certainly failed to persist a rotated refresh token. Strava, WHOOP, Oura, Garmin, and Fitbit return a NEW refresh token in the refresh response and invalidate the old one immediately. The fix is to read the refresh_token out of every refresh response and save it, overwriting the stored value. Other causes: an expired or revoked token, a missing offline scope, wrong client credentials, or clock skew.

## How to Fix Fitbit API 429 (Rate Limit) Errors

- Question: fitbit api 429 rate limit
- HTML: https://aifitnessapi.com/fix/fitbit-api-429-rate-limit
- Markdown: https://aifitnessapi.com/fix/fitbit-api-429-rate-limit.md
- Last reviewed: 2026-07-09

A Fitbit API 429 means you exceeded Fitbit's per-user hourly quota, roughly 150 requests per hour per consented user (as of 2026, verify), and every call past that is rejected until the window resets. The limit is counted per consented user, so one runaway loop on a single user trips it. To fix it, read the Fitbit-Rate-Limit-Reset or Retry-After header, wait that long, then retry with exponential backoff plus jitter. Longer term, cache responses, reduce and stagger calls, and replace polling with Fitbit subscriptions.

## Why Is HealthKit Returning No Data?

- Question: healthkit returns no data
- HTML: https://aifitnessapi.com/fix/healthkit-no-data
- Markdown: https://aifitnessapi.com/fix/healthkit-no-data.md
- Last reviewed: 2026-07-09

A HealthKit query that returns an empty array with no error is often a denied read permission, but HealthKit hides read-authorization state by design, so a blocked read is indistinguishable from a type that genuinely has no data. You cannot check read status directly. Because write status IS observable via authorizationStatus(for:), the fix is a write-then-read test: save a throwaway sample of the target type and read it back. If it round-trips, your plumbing is fine and the empty read is denied-read or truly no data; if the write fails, the problem is your Info.plist keys, HealthKit capability, or entitlement.

## Why Is Google Health Connect Returning No Data?

- Question: health connect returns no data
- HTML: https://aifitnessapi.com/fix/health-connect-no-data
- Markdown: https://aifitnessapi.com/fix/health-connect-no-data.md
- Last reviewed: 2026-07-09

The most common reason Google Health Connect returns no data is that no source app is writing that record type: Health Connect is an on-device store, not a data source, so something like Fitbit, Samsung Health, or the phone step recorder must populate it first. Open the Health Connect UI and confirm at least one app is writing the exact type you read. If a writer exists, check that your per-type read permission was actually granted (a SecurityException means it was not), that getSdkStatus returns SDK_AVAILABLE, and that you are not just hitting the default 30-day history window, which needs PERMISSION_READ_HEALTH_DATA_HISTORY for older data.

## Why Is My Strava Webhook Not Firing?

- Question: strava webhook not firing
- HTML: https://aifitnessapi.com/fix/strava-webhook-not-firing
- Markdown: https://aifitnessapi.com/fix/strava-webhook-not-firing.md
- Last reviewed: 2026-07-09

The most common reason a Strava webhook never fires is that the subscription was never created: creating one is a two-step handshake, and if your callback fails to echo the hub.challenge as JSON with HTTP 200 within about two seconds, Strava silently abandons it. First confirm a subscription actually exists by calling GET push_subscriptions with your client_id and client_secret; an empty array means nothing will ever fire. Then make sure your callback is a public HTTPS URL that answers the validation GET correctly, and remember only one subscription is allowed per application.

## Why Is Wearable Data Missing or Delayed?

- Question: wearable data missing or delayed
- HTML: https://aifitnessapi.com/fix/wearable-data-delayed
- Markdown: https://aifitnessapi.com/fix/wearable-data-delayed.md
- Last reviewed: 2026-07-09

Wearable data is near-real-time, not instant. The most common reason it looks missing is that it hasn't finished syncing device to phone app to the provider cloud yet, and a webhook fires only after the cloud has the data. Have the user force a sync in the vendor app and confirm the reading shows in the vendor's own dashboard first. If you expected history, remember a new connection only yields data from connection-time forward unless you make an explicit backfill request.

## Can't Get Garmin API Access? Here's What's Going On

- Question: garmin api access approval
- HTML: https://aifitnessapi.com/fix/garmin-api-approval
- Markdown: https://aifitnessapi.com/fix/garmin-api-approval.md
- Last reviewed: 2026-07-09

If you can't find a way to sign up for Garmin API keys, you're not doing anything wrong. Garmin's Connect Developer Program is partner-approval-only, not self-serve, and as of 2026 new sign-ups are reportedly on hold, with the public request form removed and no published re-open date. Verify the live status on developer.garmin.com, and in the meantime pull Garmin data through an aggregator like Terra that already holds its own Garmin partner access.

## Google Fit API Is Deprecated — What to Use Instead

- Question: google fit api deprecated
- HTML: https://aifitnessapi.com/fix/google-fit-api-deprecated
- Markdown: https://aifitnessapi.com/fix/google-fit-api-deprecated.md
- Last reviewed: 2026-07-09

The Google Fit API is deprecated: all Fit APIs, including the REST API, are supported only until the end of 2026, and no new developers have been able to sign up since May 1, 2024. There is no 1:1 replacement, so you must migrate based on how you used Fit. On-device reads move to Google Health Connect (plus the Recording API for steps), cloud, account, and OAuth reads move to the new Google Health API, and Wear OS moves to Health Services. Start now, because the end-of-2026 sunset is firm and new projects cannot onboard to Fit at all.

## Oura Personal Access Tokens Are Deprecated — Here's the Fix

- Question: oura personal access tokens deprecated
- HTML: https://aifitnessapi.com/fix/oura-personal-access-token-deprecated
- Markdown: https://aifitnessapi.com/fix/oura-personal-access-token-deprecated.md
- Last reviewed: 2026-08-02

Oura deprecated Personal Access Tokens around December 2025: new PATs can no longer be created, and new integrations must use OAuth 2.0 Authorization Code with scoped Bearer tokens. If a tutorial tells you to paste a personal token, it predates the change. The fix is to register an OAuth application, send users through Oura's consent screen, and exchange the code for tokens — your API calls to api.ouraring.com/v2/ stay the same, only the credential changes.

## Fitbit Error Code 401: What It Means and How to Fix It

- Question: fitbit error code 401
- HTML: https://aifitnessapi.com/fix/fitbit-error-code-401
- Markdown: https://aifitnessapi.com/fix/fitbit-error-code-401.md
- Last reviewed: 2026-08-11

Error code 401 from the Fitbit API means Fitbit rejected your credential, not your permissions: the access token is missing, malformed, expired, or no longer recognised. The usual cause is simple ageing, because a Fitbit token response carries expires_in of 28800 seconds (eight hours, verify against current docs), so refresh the token and retry. Read the errorType field inside the errors array in the response body to tell the cases apart: expired_token means refresh, while invalid_token points at a malformed header, a revoked grant, or a token minted by a different registered app. If the refresh itself fails with invalid_grant, the grant is dead and the user must authorize again.

## HealthKit Authorization Denied: What It Means and What It Hides

- Question: healthkit authorization denied
- HTML: https://aifitnessapi.com/fix/healthkit-authorization-denied
- Markdown: https://aifitnessapi.com/fix/healthkit-authorization-denied.md
- Last reviewed: 2026-08-11

In HealthKit, a denied authorization is only visible on the write side. The status returned by authorizationStatus(for:) — notDetermined, sharingDenied, or sharingAuthorized — describes permission to save data, and Apple documents that your app cannot determine whether a user granted permission to read data, because a denied read simply looks like an empty store. If your app has share permission but not read permission, Apple states you see only the samples your own app wrote, and data from other sources stays hidden. The single exception is limited authorization: when someone grants a recent window of history instead of their full history, getEarliestAuthorizedSampleDate reveals that date, and Apple calls it the only authorization state your app can positively identify.

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

- Question: strava api 401 unauthorized
- HTML: https://aifitnessapi.com/fix/strava-api-401-unauthorized
- Markdown: https://aifitnessapi.com/fix/strava-api-401-unauthorized.md
- Last reviewed: 2026-08-12

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.

## HealthKit Background Delivery Not Working: Why Your Observer Never Fires

- Question: healthkit background delivery not working
- HTML: https://aifitnessapi.com/fix/healthkit-background-delivery-not-working
- Markdown: https://aifitnessapi.com/fix/healthkit-background-delivery-not-working.md
- Last reviewed: 2026-08-12

If your HKObserverQuery never fires while your app is backgrounded, work down four documented gates before you suspect a bug. Since iOS 15 and watchOS 8 you must add the com.apple.developer.healthkit.background-delivery entitlement, which defaults to false; without it Apple documents that enableBackgroundDelivery(for:frequency:withCompletion:) fails with an HKError.Code.errorAuthorizationDenied error. Apple states on two separate pages that background server queries are not supported on the Simulator, so a Simulator test proves nothing. Observer queries must be set up in the app delegate's application(_:didFinishLaunchingWithOptions:) method so they exist before HealthKit delivers to a freshly launched process. And you must call the update's completion handler: if your app fails to respond three times, Apple documents that HealthKit assumes it cannot receive data and stops sending background updates.

## HealthKit errorDatabaseInaccessible: Reads Fail While the Device Is Locked

- Question: healthkit errordatabaseinaccessible
- HTML: https://aifitnessapi.com/fix/healthkit-database-inaccessible
- Markdown: https://aifitnessapi.com/fix/healthkit-database-inaccessible.md
- Last reviewed: 2026-09-04

HealthKit returns errorDatabaseInaccessible when your app queries the store while the device is locked. Apple's documentation states that reads fail in this state but saves still work: the data goes into a temporary file that is merged when the user unlocks the device. That makes this a background problem, because a foregrounded app is running on an unlocked device. Treat it as transient rather than terminal, resume the read after the device is unlocked, and never record the failed read as a gap in the user's history.

## HealthKit errorHealthDataUnavailable: The Device Does Not Support HealthKit

- Question: healthkit errorhealthdataunavailable
- HTML: https://aifitnessapi.com/fix/healthkit-health-data-unavailable
- Markdown: https://aifitnessapi.com/fix/healthkit-health-data-unavailable.md
- Last reviewed: 2026-09-04

Apple's documentation states that errorHealthDataUnavailable means the user accessed HealthKit on an unsupported device. Apple's discussion tells you to verify that the current device supports HealthKit before calling any other HealthKit method, because iOS apps can run on devices that do not support it. There is nothing to retry and nothing the user can change, so the only useful response is to detect the condition and hide the feature rather than showing an error. In practice the bug is usually coverage: the availability check exists in your launch path but not in the widget, extension, or background wake-up that actually made the call.

## HealthKit errorHealthDataRestricted: An MDM Profile Turned HealthKit Off

- Question: healthkit errorhealthdatarestricted mdm
- HTML: https://aifitnessapi.com/fix/healthkit-data-restricted-mdm
- Markdown: https://aifitnessapi.com/fix/healthkit-data-restricted-mdm.md
- Last reviewed: 2026-09-04

Apple's documentation states that errorHealthDataRestricted means a Mobile Device Management profile restricts the use of HealthKit on this device. Apple's discussion adds that you should verify the device supports HealthKit before calling any other HealthKit method, because a managed profile can disable it entirely. No code change, permission request, or retry will lift the restriction: only whoever administers the device can. The engineering work is therefore detection and honest messaging — model it as a distinct state, point the user at their administrator rather than at Settings, and keep a manual path so the rest of the app still works.

## HealthKit errorNoData: The Query Ran and Found Nothing

- Question: healthkit errornodata
- HTML: https://aifitnessapi.com/fix/healthkit-error-no-data
- Markdown: https://aifitnessapi.com/fix/healthkit-error-no-data.md
- Last reviewed: 2026-09-04

Apple's documentation states that errorNoData means data is unavailable for the requested query and predicate, and that the system therefore cannot calculate the query's result. It is an explicit answer, not a silent one: HealthKit is telling you the window you asked about had nothing to compute from. That makes it different from an empty sample query, which returns no error and cannot distinguish a denied read from a type nobody has ever written to. In most cases the right handling is an empty state rather than an error, with no retry and no permission prompt.

## HealthKit errorInvalidArgument: Finding the Argument HealthKit Rejected

- Question: healthkit errorinvalidargument
- HTML: https://aifitnessapi.com/fix/healthkit-invalid-argument
- Markdown: https://aifitnessapi.com/fix/healthkit-invalid-argument.md
- Last reviewed: 2026-09-04

Apple's documentation states one sentence for errorInvalidArgument — the app passed an invalid argument to the HealthKit API — and publishes no discussion paragraph. So Apple tells you an argument was rejected, and nothing about which one or why. In practice the constraint usually lives in the type rather than the call: an aggregation the type does not support, a unit from the wrong family, a predicate filtering on something the type does not have, or a reversed date range. Treat it as a programming error rather than an environmental one, log the arguments you passed, and reduce the call until the failure disappears.

## HealthKit errorAuthorizationNotDetermined: You Called Before You Asked

- Question: healthkit errorauthorizationnotdetermined
- HTML: https://aifitnessapi.com/fix/healthkit-authorization-not-determined
- Markdown: https://aifitnessapi.com/fix/healthkit-authorization-not-determined.md
- Last reviewed: 2026-09-04

Apple's documentation states that errorAuthorizationNotDetermined means the app has not yet asked the user for the authorization required to complete the task, and that it occurs when your app does not request proper authorization before calling any other HealthKit method. It is not a refusal: nobody has been asked. That distinguishes it from a denied write, which Apple describes as the user not having given the app permission to save data. The fix is ordering, and the usual cause is coverage — a widget, extension, background wake-up, watch app, or newly added type that reaches the store without passing through the request you wrote for your onboarding flow.

## HealthKit errorRequiredAuthorizationDenied: Clinical Records Are a Separate Class

- Question: healthkit errorrequiredauthorizationdenied
- HTML: https://aifitnessapi.com/fix/healthkit-required-authorization-denied
- Markdown: https://aifitnessapi.com/fix/healthkit-required-authorization-denied.md
- Last reviewed: 2026-09-04

Apple's documentation states that errorRequiredAuthorizationDenied means the user has not granted the application authorization to access all the required clinical record types. The load-bearing word is all: this is not one refused permission but an incomplete set. Apple's discussion adds that you specify those required clinical record types with an Info.plist key, so the requirement lives in your app's configuration rather than in the request itself. Because Apple states the system does not tell your app which record types were denied, the only real lever you have is declaring fewer required types and designing a path that still works without them.

## HealthKit Workout Session Errors: Four Cases, Two of Them Undocumented

- Question: healthkit workout session errors
- HTML: https://aifitnessapi.com/fix/healthkit-workout-session-errors
- Markdown: https://aifitnessapi.com/fix/healthkit-workout-session-errors.md
- Last reviewed: 2026-09-04

Four HKError cases end a workout session, and Apple describes only two of them. Apple's documentation states that errorAnotherWorkoutSessionStarted means another app started a session, and that Apple Watch runs one workout session at a time, so your session receives the error and then ends while the second one starts. Apple states that errorUserExitedWorkoutSession means the user exited your application while a session was running, and that workout sessions end when the app goes into the background. The remaining two, errorBackgroundWorkoutSessionNotAllowed and errorWorkoutActivityNotAllowed, are published with no abstract at all, so treat them as unknown codes rather than inferring behaviour. In every case the session is already gone, which makes continuous persistence the only real defence.

## HealthKit errorNotPermissibleForGuestUserMode: Writes Blocked in a Guest Session

- Question: healthkit errornotpermissibleforguestusermode
- HTML: https://aifitnessapi.com/fix/healthkit-guest-user-mode
- Markdown: https://aifitnessapi.com/fix/healthkit-guest-user-mode.md
- Last reviewed: 2026-09-04

Apple's documentation states that errorNotPermissibleForGuestUserMode means the app attempted to write HealthKit data while in a Guest User session in visionOS, and publishes no discussion beyond that abstract. Apple's guest-session guidance, quoted in our authorization guide, adds that permissions do not change during a guest session, so your status check still reports the owner's grant while the save fails. Apple also states the authorization sheet is not displayed, so requests during a guest session fail silently, and suggests silently ignoring the write error for passive or periodic saves. The practical handling is to buffer the data, stay quiet unless the guest explicitly asked to save, and never treat the failure as a denial.

## Undocumented HealthKit Errors: Handling a Case Apple Never Described

- Question: undocumented healthkit error codes
- HTML: https://aifitnessapi.com/fix/healthkit-undocumented-errors
- Markdown: https://aifitnessapi.com/fix/healthkit-undocumented-errors.md
- Last reviewed: 2026-09-04

Several HKError cases are published with no description whatsoever: unknownError, errorDataSizeExceeded, errorBackgroundWorkoutSessionNotAllowed, and errorWorkoutActivityNotAllowed all appear as type properties with no abstract and no discussion. Apple documents nothing about when any of them is returned, and the oldest of them has been present since the earliest HealthKit releases without ever acquiring one. A name can legitimately point you at where to look in your own app; it cannot tell you what the framework decided. The workable strategy is to log the raw domain and code, fail soft without discarding the user's data, bound your retries, and keep your inferences labelled as inferences.
