HTTP 400 Bad Request: {"error":"invalid_redirect_uri"}
Nothing about your identity was tested here. This failure happens before any credential is presented: the authorization server compared the callback address your client asked it to return to against the list of addresses registered for that client, found no exact match, and stopped. So it is not a wrong API key, not an expired token, not a permissions problem, not a quota problem, and not a network problem — the request arrived and was answered. It is a registration mismatch between two records you may not both control.
The body shape is a second clue worth reading. A platform API error carries
structured fields — a type, a code, a param, a message. This body is a single
error string, which is the OAuth 2.0 authorization-server error format. The
thing that refused you is an authorization endpoint, not the API you were
trying to reach.
Exact string matching, and everything it counts
The redirect URI is compared as a string, not as a URL a human would consider equivalent. All of these are different values, and a provider that has one registered will reject the others:
http://localhost:1455/callbackandhttp://127.0.0.1:1455/callback— the loopback hostname and the loopback address are not the same string.- The same URI with and without a trailing slash.
httpversushttpson the same host.- Any difference in port, including a port that is present in one and absent in the other.
- A path that differs in case, or that carries an extra query parameter.
The most common reason this error looks intermittent is a port that changes. A desktop client that opens a loopback listener frequently binds an ephemeral port and builds its callback from whatever it got. If the provider’s registration lists one fixed port, the flow succeeds on the runs that happen to get that port and fails on the rest — which reads as randomness and sends people looking for a flaky network. If your failures come and go with no config change between them, capture the actual redirect URI on a failing run and on a working one and compare them character by character. One of those two runs is not using the port you think it is.
Where it fails decides who can fix it
The redirect URI is sent twice in an authorization-code flow: once when the browser is sent to the authorization endpoint, and again when the client exchanges the code for a token. The provider requires it to match its registration the first time, and to match the value used in the first request the second time. Those are two distinct checks, and they fail for different reasons.
It fails before you ever see a consent screen. The browser lands on an error page or the CLI prints this string immediately. The value your client sent is not in the provider’s registered list. This is the registration case.
The consent screen appeared, you approved it, and the failure came at the exchange. The registration is fine — you just proved it — and your client is computing the redirect URI differently in the two steps. This happens when a value is rebuilt from scratch rather than carried forward: a different host name, a re-bound port, a normalized trailing slash. This is a client bug, or a client misconfiguration, and no amount of editing the provider’s registration will change it.
That distinction is the whole triage. Note which step failed before you touch anything.
Is retrying useful?
No. The comparison is deterministic and both sides of it are unchanged by a second attempt.
There is no server asking you to wait, no retry-after to honor, and no
documented base delay to copy — so any pause you insert is a number you invented.
Running the sign-in again sends the identical redirect URI to the identical
registration and receives the identical refusal.
The one exception is the ephemeral-port case above, where a retry can appear to work because the client happened to bind the registered port that time. Treat that as a diagnosis, not a fix: a flow that succeeds one run in five will fail in front of a colleague, in CI, and on the machine you least want to debug.
If a retry is cheap and you want one data point, run it once with the redirect URI logged. Then stop and compare strings.
The fix depends on which client this is — and often it is not yours
This is the part that saves the most time, and it is the part most write-ups skip.
You registered the app yourself. Add the exact URI your client sends to the application’s allowed redirect URIs in the provider’s developer settings, then re-run the flow. Copy the value from the failing request rather than typing what you believe it to be; the difference is usually the character you would have typed correctly.
You are using a client someone else registered — an MCP server, a CLI, an IDE extension, a hosted connector. The client identifier and the registered redirect URIs belong to that application’s owner. There is no local setting that makes the provider accept an unregistered address, and changing your own callback configuration to something you prefer will only move the failure. What you can do: check whether the tool documents a supported callback (some accept a fixed port or a configured base URL for exactly this reason), check whether the provider’s app settings are under your organization’s control, and otherwise report the URI it sends to whoever maintains it. That report is short and actionable: “the flow sends this URI, the registration does not list it.”
Your organization administers the provider side. Then the person who can add the entry is an administrator of that workspace or developer account, not you, and the fastest path is a one-line request naming the exact string.
Do not paste tokens, authorization codes, or full callback URLs into an issue tracker, a chat window, or a third-party diagnostic site while reporting this. A callback URL carries the authorization code in it and is as sensitive as a credential for as long as that code lives. Redact everything after the path: the host, port and path are the whole diagnosis, and the query string is not.
How to confirm it’s fixed
Log the redirect URI your client sends, on both steps of the flow, and require them to be byte-identical to each other and to the registered entry. That assertion is the fix; a sign-in completing is a side effect of it.
Then run the flow twice from a cold start, killing the process in between. One success proves nothing when the suspected cause is a port that varies between runs — two cold starts that both complete is the cheapest evidence that the address is now stable and registered. If you are on a machine that uses a proxy or a VPN, do the second run on the network you actually work from.
Related errors
- openai.error.AuthenticationError: <empty message> is the failure that happens after a sign-in works — a credential existed and was refused — which is a different diagnosis in a different place.
- invalid_api_key Incorrect API key provided: undefined is worth reading if your trouble started right after you gave up on the OAuth flow and wired in a key by hand.
- MCP client failed to start: handshaking with MCP server failed is what the same broken authorization looks like from the tool’s side when the server never becomes usable.
- The AI coding error triage tool separates sign-in failures from credential failures from transport failures, which is the first fork this error gets put on the wrong side of.