What Is a Callback URL in a Connected App?
A callback URL is where an OAuth provider returns the user after authorization. Learn what to enter, how exact matching works, and how to secure it.
Direct answer: A callback URL is the endpoint in your application where an OAuth provider returns the user after authorization. In Salesforce, it is the same setting as the OAuth redirect URI. Microsoft Entra calls the equivalent value a redirect URI or reply URL.
For a web application, enter the HTTPS URL your server actually handles, such as https://app.example.com/oauth/callback. Send that identical value in the authorization request’s redirect_uri parameter. The provider compares the runtime value with the URLs registered in the connected app or identity-platform app registration. A mismatch causes validation or approval failure.
How a callback URL works
- Your application builds an authorization URL containing the client ID, requested scopes, response type,
redirect_uri, and a randomstatevalue. - The user signs in at the provider and approves access.
- The provider redirects the browser to your callback URL. In an authorization-code flow, the URL normally contains a short-lived
codeand the originalstate. - Your callback handler validates
state, then your server exchanges the code at the provider’s token endpoint. Keep this exchange on the server so tokens are not exposed to browser scripts.
The callback URL is therefore a return endpoint. It is not the provider’s login URL and it is not the token endpoint.
What to enter in a Salesforce connected app
In the connected app’s OAuth settings, enter the endpoint your application handles, for example:
https://app.example.com/oauth/callback
Salesforce documents the field as the endpoint it calls during OAuth and treats it as the OAuth redirect URI. Your authorization request must contain the same value, URL-encoded when placed in a query string. See the Salesforce connected app documentation.
For local development, Salesforce’s developer guidance gives this example:
http://localhost:1717/OauthRedirect
Change the port and path to match your local handler. Register every environment deliberately if you need more than one callback. Salesforce matches the runtime value against the configured values, so the supplied value must be one of them.
Exact matching rules
Compare the registered value and the value sent in redirect_uri character by character:
| Part | Must match | Common mistake |
|---|---|---|
| Scheme | https versus http |
Using HTTP in production when HTTPS is registered |
| Host | Exact hostname | www.example.com versus example.com |
| Port | Exact non-default port | Registering :3000 but running on :3001 |
| Path | Exact path and capitalization where applicable | /oauth/callback versus /oauth/callback/ |
| Query and fragment | Follow the provider’s registration rules | Adding an unregistered query parameter |
| Encoding | The decoded URL must equal a registered URL | Double-encoding the callback value |
Microsoft Entra describes the same rule: the redirect URI must exactly match one registered in the admin center, with the request value URL-encoded. Its terminology and registration guidance are in the Microsoft Entra redirect URI documentation.
Complete authorization-code example
The following example uses a generic provider. Replace the authorization and token endpoints, client ID, scopes, and secret with values from your provider.
Node.js with Express
import crypto from 'node:crypto';
import express from 'express';
const app = express();
const port = 3000;
const callbackUrl = 'http://localhost:3000/oauth/callback';
const clientId = process.env.CLIENT_ID;
const clientSecret = process.env.CLIENT_SECRET;
const authorizationEndpoint = 'https://provider.example.com/oauth/authorize';
const tokenEndpoint = 'https://provider.example.com/oauth/token';
app.get('/login', (req, res) => {
const state = crypto.randomBytes(32).toString('hex');
res.cookie('oauth_state', state, { httpOnly: true, sameSite: 'lax' });
const query = new URLSearchParams({
response_type: 'code',
client_id: clientId,
redirect_uri: callbackUrl,
scope: 'read',
state
});
res.redirect(`${authorizationEndpoint}?${query}`);
});
app.get('/oauth/callback', async (req, res) => {
const { code, state, error, error_description: errorDescription } = req.query;
if (error) return res.status(400).send(`Authorization failed: ${errorDescription || error}`);
if (!code || !state || state !== req.cookies?.oauth_state) {
return res.status(400).send('Invalid OAuth callback');
}
const tokenResponse = await fetch(tokenEndpoint, {
method: 'POST',
headers: { 'content-type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
code,
client_id: clientId,
client_secret: clientSecret,
redirect_uri: callbackUrl
})
});
if (!tokenResponse.ok) return res.status(502).send('Token exchange failed');
const tokens = await tokenResponse.json();
// Store tokens securely on the server. Do not render them in the response.
res.send('Authorization complete');
});
app.listen(port, () => console.log(`Listening on http://localhost:${port}`));
This sample expects cookie middleware in a real application. Use your framework’s secure, HTTP-only session mechanism for the state value, and add error handling around network and token parsing failures.
Python with Flask
import os
import secrets
from flask import Flask, redirect, request, session
import requests
app = Flask(__name__)
app.secret_key = os.environ['SESSION_SECRET']
CALLBACK_URL = 'http://localhost:5000/oauth/callback'
AUTHORIZATION_ENDPOINT = 'https://provider.example.com/oauth/authorize'
TOKEN_ENDPOINT = 'https://provider.example.com/oauth/token'
@app.get('/login')
def login():
state = secrets.token_urlsafe(32)
session['oauth_state'] = state
params = {
'response_type': 'code',
'client_id': os.environ['CLIENT_ID'],
'redirect_uri': CALLBACK_URL,
'scope': 'read',
'state': state,
}
return redirect(requests.Request('GET', AUTHORIZATION_ENDPOINT, params=params).prepare().url)
@app.get('/oauth/callback')
def callback():
if request.args.get('error'):
return f"Authorization failed: {request.args.get('error')}", 400
if request.args.get('state') != session.pop('oauth_state', None):
return 'Invalid OAuth state', 400
code = request.args.get('code')
if not code:
return 'Missing authorization code', 400
response = requests.post(TOKEN_ENDPOINT, data={
'grant_type': 'authorization_code',
'code': code,
'client_id': os.environ['CLIENT_ID'],
'client_secret': os.environ['CLIENT_SECRET'],
'redirect_uri': CALLBACK_URL,
}, timeout=15)
response.raise_for_status()
tokens = response.json()
# Store tokens in a protected server-side store.
return 'Authorization complete'
if __name__ == '__main__':
app.run(port=5000, debug=True)
cURL request inspection
Use cURL to inspect the authorization URL and verify that the callback value is encoded as expected. The provider still needs a browser for sign-in and consent.
curl -G 'https://provider.example.com/oauth/authorize' \
--data-urlencode 'response_type=code' \
--data-urlencode 'client_id=YOUR_CLIENT_ID' \
--data-urlencode 'redirect_uri=http://localhost:3000/oauth/callback' \
--data-urlencode 'scope=read' \
--data-urlencode 'state=RANDOM_STATE_VALUE'
Production, localhost, and mobile callbacks
- Production web app: use a public HTTPS endpoint such as
https://app.example.com/oauth/callback. - Local development: localhost is appropriate when the provider permits it. Keep it in a development registration and use the exact port and path your process listens on.
- Native mobile app: Salesforce documents custom URI schemes for suitable native flows, such as
myapp://oauth/callback. The scheme must match the mobile project and provider configuration. Identity-provider use cases may require HTTPS. - Separate environments: keep development and production registrations separate where practical so localhost and test hosts are not exposed in a production app.
Security checklist for the callback handler
- Generate a cryptographically random
stateper authorization request and reject callbacks with a missing or mismatched value. - Use authorization code flow with a server-side token exchange when your provider supports it.
- Never put client secrets, access tokens, or refresh tokens in URLs, browser-rendered pages, analytics events, or ordinary logs.
- Validate the expected error and code parameters, handle provider errors, and expire one-time state values.
- Use HTTPS outside local development and protect the callback session cookie.
- Register only callback URLs you control. Remove old test endpoints when an environment is retired.
Testing and troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Redirect URI mismatch | Scheme, host, port, path, slash, case, or encoding differs | Copy the registered value and the request value into a diff tool; make them identical and URL-encode only for transport. |
| Callback never arrives | Handler is not reachable, route is wrong, or the provider returned an error | Open the route locally, inspect provider parameters, and verify the authorization request selected the intended environment. |
| Invalid state | State was not stored, expired, or belongs to another browser session | Persist state in a secure session, allow one callback per state, and avoid load-balancer nodes that cannot share sessions. |
| Missing code | User denied consent or provider returned an error response | Handle error and error_description; do not assume every callback contains a code. |
| Token exchange rejected | The exchange uses a different redirect_uri or client credentials |
Send the same registered callback value during token exchange and verify the client ID and secret. |
| Works locally but not in production | Production hostname is not registered or proxy rewrites the URL | Register the public HTTPS URL and configure your proxy’s forwarded host and scheme correctly. |
| Mobile redirect opens a browser | Custom scheme is not registered by the app or does not match configuration | Register the exact scheme and path in the mobile project and connected app. |
Performance, reliability, and cost
The redirect itself should be fast: validate the request, exchange the code, persist tokens, and send a short success response. Do not perform long imports or other slow work before responding. Queue background work after the token exchange when possible.
For reliability, make the callback idempotent. A user can refresh a callback page, a reverse proxy can retry a request, or a provider can deliver an error after a timeout. Store a one-time authorization transaction and reject reused codes safely. Log a correlation ID and outcome, while redacting codes, tokens, and secrets.
OAuth providers generally do not charge for receiving a callback; your costs come from hosting, logging, database storage, and any provider API usage after token acquisition. Set timeouts on token requests and retry only safe, transient network failures. Never retry a one-time authorization code blindly.
Or skip the browser setup
If your goal is to document, monitor, or visually verify a callback endpoint, ScreenshotNeo can capture the resulting page with one request. Its API removes cookie banners, newsletter popups, and chat widgets before the shot. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and each response reports the page verdict and billing status in headers. An MCP server lets Claude, Cursor, and other MCP clients use take_screenshot, get_page_info, and capture_pdf.
See the ScreenshotNeo API documentation for all options.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://app.example.com/oauth/callback -o callback.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://app.example.com/oauth/callback"}, timeout=90)
open("callback.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://app.example.com/oauth/callback' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
There are 1,000 free screenshots a month with no card; paid plans start at $5 for 3,000. Create a free ScreenshotNeo account.
FAQ
Is a callback URL the same as a redirect URI?
Yes. Salesforce calls it a callback URL; OAuth specifications and Microsoft Entra commonly call it a redirect URI or reply URL.
Can I register more than one callback URL?
Usually yes. Register each deliberate environment or platform endpoint and send exactly one registered value in each authorization request.
Should the callback URL include the provider’s login page?
No. The callback is your application’s endpoint. The provider’s authorization endpoint is where you send the user first.
Can a callback URL be localhost?
Yes for development when the provider allows it. Use HTTPS and a public hostname for production.
What changed for Salesforce connected apps?
Salesforce says connected-app creation is restricted as of Spring ’26, while existing connected apps remain usable. Salesforce recommends external client apps for new creation.


