How to Fix the OAuth redirect_uri Mismatch Error
Updated July 9, 2026
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):
httpvshttps. You registeredhttps://...but your local dev server sendshttp://localhost...(or the reverse). The schemes must match exactly.localhostvs127.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 toexample.comvswww.example.com.- Trailing slash.
/callbackand/callback/are different. This is the sneakiest one because frameworks and proxies sometimes add or strip the slash for you. - Port present vs absent.
http://localhost:3000/callbackis nothttp://localhost/callback, andhttps://app.example.com:443/callbackis nothttps://app.example.com/callback(443 is implicit for https). Register the port exactly as your code emits it. - Path differences (including case).
/callbackvs/oauth/callbackvs/Callback. Paths are case-sensitive to the matcher. - URL-encoding differences.
%2Fvs/, or uppercase vs lowercase in a percent-encoded segment. Encode theredirect_uriquery parameter once and consistently; do not double-encode it. - The two steps disagree. The
redirect_urisent 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 asinvalid_grantrather thanredirect_uri_mismatch.
Why you sometimes see
invalid_grantinstead. Per RFC 6749, the token endpoint returnsinvalid_grantwhen the grant "does not match the redirection URI used in the authorization request." So a redirect mismatch caught at the authorize step reads asredirect_uri_mismatch, but the same mismatch caught at the token step can read as400 invalid_grant. Always log theerror_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 —
httpvshttps - host —
localhostvs127.0.0.1, apex vswww - port — present vs absent, explicit
:443vs implicit - path — spelling and case,
/callbackvs/oauth/callback - trailing slash —
/callbackvs/callback/ - query string — any extra or reordered params
- percent-encoding —
%2Fvs/, 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_uriat 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(notredirect_uri_mismatch), readerror_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