Skip to content
AFAIFitnessAPI
Troubleshooting

How to Fix the OAuth redirect_uri Mismatch Error

Updated July 9, 2026

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.

You clicked through the OAuth consent screen (or you POSTed the authorization code) and the provider threw back redirect_uri_mismatch — or, at the token step, an unhelpful invalid_grant. The cause is almost always the same: the redirect_uri your code sent is not byte-for-byte identical to a callback URL registered in the provider's developer console. Fix it by copying the registered value and the sent value side by side and diffing them character for character — scheme, host, port, path, trailing slash, query, and encoding all count.

OAuth servers do an exact string comparison on the redirect URI, not a semantic one. https://app.example.com/callback and https://app.example.com/callback/ are two different strings to the server, even though a browser treats them the same. That single rule explains nearly every mismatch on this page.

Most likely causes (ranked)

From most to least common in fitness/health API integrations (Fitbit, Strava, Oura, WHOOP, Garmin):

  1. http vs https. You registered https://... but your local dev server sends http://localhost... (or the reverse). The schemes must match exactly.
  2. localhost vs 127.0.0.1. These are not equal to the string matcher, even though they resolve to the same machine. Pick one and register that exact host. The same applies to example.com vs www.example.com.
  3. Trailing slash. /callback and /callback/ are different. This is the sneakiest one because frameworks and proxies sometimes add or strip the slash for you.
  4. Port present vs absent. http://localhost:3000/callback is not http://localhost/callback, and https://app.example.com:443/callback is not https://app.example.com/callback (443 is implicit for https). Register the port exactly as your code emits it.
  5. Path differences (including case). /callback vs /oauth/callback vs /Callback. Paths are case-sensitive to the matcher.
  6. URL-encoding differences. %2F vs /, or uppercase vs lowercase in a percent-encoded segment. Encode the redirect_uri query parameter once and consistently; do not double-encode it.
  7. The two steps disagree. The redirect_uri sent to the authorize step and the one sent to the token step must be the same string. If they differ, you may pass the authorize step and then get rejected at the token exchange — where it can surface as invalid_grant rather than redirect_uri_mismatch.

Why you sometimes see invalid_grant instead. Per RFC 6749, the token endpoint returns invalid_grant when the grant "does not match the redirection URI used in the authorization request." So a redirect mismatch caught at the authorize step reads as redirect_uri_mismatch, but the same mismatch caught at the token step can read as 400 invalid_grant. Always log the error_description — providers put the specific reason there.

How to fix it

Step 1: Print the exact URI your code sends

Before anything else, log the literal redirect_uri string your app puts on the wire, at both the authorize step and the token step. Do not read it from a config variable you assume is correct — log the actual outgoing value.

# Whatever your code builds, echo the exact string it sends:
echo "$REDIRECT_URI"
# e.g. http://localhost:3000/callback

Step 2: Copy the registered URI from the developer console

Open the provider's app settings (Fitbit dev.fitbit.com, Strava's API settings, Oura, WHOOP, or the Garmin developer portal) and copy the registered Callback / Redirect URI verbatim. Paste both values into a plain-text editor, one above the other.

Step 3: Diff them character by character

Walk the two strings left to right and check each component. A quick way to catch invisible differences (trailing slash, whitespace, encoding) is to compare them programmatically rather than by eye:

# Prints "MATCH" only if the two strings are byte-for-byte identical.
REGISTERED="https://app.example.com/callback"
SENT="https://app.example.com/callback/"
[ "$REGISTERED" = "$SENT" ] && echo "MATCH" || echo "MISMATCH"
# MISMATCH  <- the trailing slash on $SENT is the bug

Use this checklist as you compare:

  • scheme — http vs https
  • host — localhost vs 127.0.0.1, apex vs www
  • port — present vs absent, explicit :443 vs implicit
  • path — spelling and case, /callback vs /oauth/callback
  • trailing slash — /callback vs /callback/
  • query string — any extra or reordered params
  • percent-encoding — %2F vs /, and the case of encoded characters

Step 4: Make the two OAuth steps use the identical string

Keep the redirect_uri in one constant and pass that same constant to both the authorize URL and the token exchange. If they diverge, the token step rejects you.

# Authorization-code exchange (Strava shown). The redirect_uri here MUST equal
# the one used to build the authorize URL earlier — same string, character for character.
curl -X POST https://www.strava.com/oauth/token \
  -d client_id=$CID -d client_secret=$SECRET \
  -d code=$AUTH_CODE \
  -d grant_type=authorization_code \
  -d redirect_uri=https://app.example.com/callback
# A 400 invalid_grant here is often the redirect_uri differing from the authorize step
# (or a reused/expired code) — read error_description to disambiguate.

Step 5: Register every environment explicitly

Local dev, staging, and production each need their own registered callback (for example http://localhost:3000/callback, https://staging.example.com/callback, and https://app.example.com/callback). Add each one to the console rather than trying to make one entry cover all environments.

Step 6: Never append dynamic data to the redirect URI

Do not tack per-request query parameters onto redirect_uri — an unregistered or reordered query string will fail the exact-match check. Put per-request data in the state parameter instead, which is designed for exactly this and also protects against CSRF.

Still stuck? Quick triage

Run this short checklist:

  • Log the literal sent redirect_uri at both steps and diff each against the console value with a string-equality check, not your eyes.
  • Confirm scheme, host, port, path, trailing slash, query, and encoding all match — this covers essentially every case.
  • Confirm the authorize step and token step send the same string.
  • If you only see invalid_grant (not redirect_uri_mismatch), read error_description; it may be a reused or expired authorization code rather than the URI — codes are single-use and short-lived.
  • If your app sits behind a proxy or framework that rewrites the path, verify what actually leaves your process, since the proxy may add or strip a trailing slash after your code builds the URL.

Once the callback matches, you're back on the happy path. For the full authorization flow per provider, see the integration guides — for example Fitbit, Strava, or Garmin. And if your calls start returning 401 after auth succeeds, that's a different problem — head to Fix a fitness API 401 Unauthorized error.

Frequently asked questions

Why do localhost and 127.0.0.1 count as a mismatch?
OAuth servers compare the redirect_uri as an exact string, not by what it resolves to. localhost and 127.0.0.1 are different strings even though they point to the same machine, so you must register and send whichever one your code actually uses.
Does a trailing slash really matter?
Yes. /callback and /callback/ are different strings to the matcher, so one will fail if the other is registered. Frameworks and proxies sometimes add or strip the slash after your code builds the URL, so verify what actually leaves your process.
Why do I get invalid_grant instead of redirect_uri_mismatch?
A redirect mismatch caught at the authorize step reads as redirect_uri_mismatch, but the same mismatch caught at the token step can read as 400 invalid_grant because the grant no longer matches the redirect used in the authorization request. Read the error_description field, since invalid_grant can also mean a reused or expired authorization code.
Do the authorize and token steps need the same redirect_uri?
Yes. The redirect_uri sent when you build the authorize URL and the one sent when you exchange the code must be the identical string. If they differ you may pass the authorize step and then be rejected at the token exchange.
How do I support local, staging, and production callbacks?
Register each environment's exact callback URL separately in the developer console, for example an http localhost URL for dev and https URLs for staging and production. Do not try to make a single entry cover multiple environments.

Keep reading

Independent comparison, last reviewed July 9, 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