Shopify OAuth Callbacks: URLs, Parameters, and Validation Order
application_url, redirect_urls, and the redirect_uri in an OAuth request are often confused. All three hold URLs, but each does a different job: one is the app's entry point, one is the list of permitted callbacks, one is the address this particular authorization returns to.
This article is about standalone or API-only apps that run outside the Shopify admin and use the authorization code grant. Embedded apps built from the Shopify CLI template normally use Shopify-managed installation with token exchange, and should not have a legacy installation callback bolted on just to match this article. See the authentication and authorization overview.
What each URL is for
| Name | Purpose | Who uses it |
|---|---|---|
application_url | The home page a merchant lands on when opening the app | Shopify admin and app navigation |
redirect_urls | The allowlist of OAuth callback addresses | App configuration and Shopify's authorization service |
redirect_uri | The address this authorization actually returns to | The authorization request your app builds |
Shopify requires the redirect_uri in the authorization request to match a configured redirect URL exactly — this is not prefix matching. See Authenticate standalone apps.
redirect_urls is not a list of business pages to send users to after a successful login. Return to a fixed authentication endpoint, validate, establish a session, and let the app decide the final destination from there.
The authorization and callback flow
sequenceDiagram
participant M as Merchant browser
participant A as App backend
participant S as Shopify
M->>A: Start install or authorization
A->>A: Generate and store a random state
A-->>M: Redirect to the Shopify authorization screen
M->>S: Grant the requested scopes
S-->>A: Call back to redirect_uri
A->>A: Validate shop, state, HMAC, and timing
A->>S: Exchange the code for an access token
S-->>A: Return the token
A-->>M: Enter the app UI
Shopify's current authorization code grant documentation states that the callback carries code, hmac, shop, state, and timestamp, and that the redirect_uri must exactly match a redirect URL registered in the Dev Dashboard or the app's configuration TOML. Do not treat a parameter such as host, which appears at other entry points, as guaranteed by this callback contract; go by the authentication method and framework contract you are actually using.
state carries the dynamic context
Generate a random nonce when authorization starts and associate it with the server-side context you need to restore:
state → {
expectedShop,
returnPath,
organizationId,
appKey,
expiresAt
}
Compare state on the callback and invalidate it after use. That prevents CSRF and removes any reason to put organization IDs, internal return paths, or permission information into the redirect_uri.
Never trust unsigned dynamic context in the callback query. Even with a signed cookie or an encrypted state, keep it short-lived and single-use.
Fixed query parameters in the callback URL
OAuth 2.0 allows a redirect URI to include a query component of its own, which the authorization server must keep when it appends the callback parameters. See RFC 6749 §3.1.2.
For example:
[auth]
redirect_urls = [
"https://apps.example.com/auth/callback?surface=console"
]
The redirect_uri you send when starting authorization then has to be that same complete address. Fixed parameters must not collide with Shopify's callback parameter names, and HMAC verification must not drop any field from the callback query.
Fixed parameters are fine for distinguishing stable entry points. They are the wrong place for per-request organization, user, or final destination — that belongs in server-side state.
A validation order that works
- Parse the request but do not use
codeyet. - Check that
shopis an allowed Shopify store domain, with a regular expression anchored at both ends, for example^[a-zA-Z0-9][a-zA-Z0-9\-]*\.myshopify\.com$. - Read and consume the server-side
stateexactly once, checking store, app, and expiry. - Verify the HMAC: remove
hmacitself from the callback parameters, sort the rest alphabetically, compute HMAC-SHA256 with that app's client secret, and compare in constant time. - Check the timestamp or request freshness and reject obvious replays.
- Exchange the code using the same app's client ID and secret and the same
redirect_uri. - Persist the installation record and the session.
- Redirect only to an internal path that was allowed in advance.
Do not bind a store before HMAC and state have passed, and do not return the authorization code or an access token to a business URL the browser can read.
Several apps on one domain
Two Shopify Apps can share a domain, but separate callback paths are better:
/shopify/public/auth/callback
/shopify/custom/auth/callback
The path establishes app identity first, and the server then picks the matching secret. Even if both apps registered the same callback address, each would still have its own client ID, secret, scopes, sessions, and access tokens; a token belongs to a specific app-and-store installation, not to a domain.
Full backend separation for multiple apps is covered in Two Shopify Apps on One Backend.
Troubleshooting checklist
- Does the authorization request's
redirect_uriexactly match the configured address? - Which app configuration is running, and is it using another environment's client ID?
- Does
stateexist, is it unexpired, single-use, and tied to this store and app? - Is the HMAC verified over the complete, normalized callback parameters?
- Does the callback depend on a parameter the current authentication contract does not guarantee?
- Does the token exchange use the same
redirect_uriand the correct secret? - Is the final redirect restricted to an allowed internal path?
The callback URL is only the entrance. OAuth security comes from the entrance, the server-side state, the signature check, and the app credentials all belonging to the same authorization chain.