Back to HomeReference

Shopify OAuth Errors: Codes, Causes & Fixes

Every common error you’ll hit during the Shopify OAuth flow and when calling the Admin API with the resulting token. Each one with the symptom, cause, and exact fix.

By Datora Team · Updated

Where errors happen

The Shopify OAuth 2.0 Authorization Code flow has three places where errors surface, and the fix is different in each:

  1. Authorization request — you build a URL pointing at /admin/oauth/authorize; Shopify rejects it before the merchant sees the consent screen.
  2. Authorization callback — the merchant approved (or canceled), and Shopify redirects back to your callback URL with either a code or an error.
  3. Token usage — you have an access token and you’re calling the Admin API. The error is from your API call, not from OAuth itself.

Knowing which phase produced the error narrows the fix dramatically. The reference below is grouped by phase. Need a refresher on the flow itself? Read the step-by-step guide.

Error reference

Every common error, explained

Authorization request errors

Returned before the merchant ever sees the consent screen — Shopify rejects your authorization URL.

invalid_request

The browser lands on Shopify with a generic error page or your callback receives error=invalid_request.

+
Cause

A required parameter is missing or malformed. Most often: missing client_id, empty scope, missing redirect_uri, or a shop parameter that isn't a valid *.myshopify.com domain.

Fix

Inspect your authorization URL parameter by parameter. Required: client_id, scope (comma-separated, no spaces), redirect_uri (must exactly match what's registered in the app settings, including trailing slashes), state (your CSRF nonce), and a valid shop. Compare against a known-working URL.

invalid_client

Token exchange POST to /admin/oauth/access_token returns invalid_client.

+
Cause

The client_id and/or client_secret in your token-exchange request don't match a real Shopify app, or you copied the wrong app's credentials.

Fix

In your Dev Dashboard, open the app's API credentials page and copy the Client ID and Client Secret again. Be careful not to copy whitespace. Verify you're using the credentials of the same app that initiated the authorization request.

invalid_scope

Authorization URL is rejected with error=invalid_scope before the consent screen.

+
Cause

A scope name is misspelled, doesn't exist, or you're requesting a protected scope (like read_all_orders) without approval.

Fix

Scope names are case-sensitive snake_case (read_products, write_orders). See the complete scope reference for valid names. For protected scopes, apply for access through your Partner Dashboard before requesting them.

redirect_uri mismatch

Authorization URL is rejected, or the callback never fires after consent.

+
Cause

The redirect_uri in your authorization request doesn't exactly match any URL listed in the app's Allowed redirection URLs.

Fix

Open your app settings on the Dev Dashboard and verify the Allowed redirection URLs list. The match is exact: scheme, host, port, path, and trailing slash all matter. http://localhost:3000/callback and https://localhost:3000/callback are different. Add the URL you're actually using.

HMAC verification failed

Your callback handler rejects the request because the hmac query parameter doesn't match what you computed.

+
Cause

The HMAC signature Shopify sends covers the rest of the query parameters, signed with your client secret. Mismatch usually means you're hashing the wrong string or using the wrong secret.

Fix

Build the message to hash from all query parameters except hmac and signature, sorted alphabetically, joined as key=value pairs with & separators. Do not URL-encode. Hash with HMAC-SHA256 using your client secret. Compare hex-to-hex. Most bugs are: leaving hmac in the input, double-URL-decoding values, or using the wrong app's secret.

Invalid shop parameter

Authorization URL rejected with an error about the shop parameter, or 404 from Shopify.

+
Cause

The shop parameter must be a valid *.myshopify.com domain (the canonical one, not a custom domain). Subdomains, custom domains, and trailing paths all fail.

Fix

Always resolve the merchant's input to its canonical *.myshopify.com domain before building the OAuth URL. If the user types a custom domain like example.com, you can resolve it via a HEAD request to https://example.com and following redirects to find the .myshopify.com canonical.

Authorization callback errors

The merchant saw the consent screen, but something went wrong before you got a working token.

access_denied

Your callback receives error=access_denied&error_description=...

+
Cause

The merchant clicked Cancel on the consent screen instead of approving the install. This is user behavior, not a bug.

Fix

There's no programmatic fix. Reduce your scope requests (merchants reject installs that ask for too much), improve your in-app messaging that explains why you need each scope, and let the user retry the flow.

invalid_grant

Token exchange POST returns invalid_grant.

+
Cause

The authorization code is single-use, expires after roughly 10 minutes, and is bound to the exact client_id and redirect_uri that requested it. You're either reusing a code, exchanging it too late, or sending mismatched parameters.

Fix

Exchange the code immediately on callback, server-side. Never store the code for later. Never retry a failed exchange with the same code — start the flow over. Confirm client_id and redirect_uri in the exchange match what was used in the authorize request.

state parameter mismatch

Your callback sees a state value that doesn't match what you stored before redirecting.

+
Cause

Either someone is replaying an old callback URL, your state-storage cookie expired or didn't make the round-trip, or you're storing state per-tab and the user opened the consent screen in a different tab.

Fix

Treat state mismatch as a real CSRF rejection — refuse the callback. If users hit this legitimately, extend your session storage TTL beyond the OAuth window, store state in a cookie that survives navigation, and make sure your cookie's SameSite policy doesn't strip it on cross-site redirects.

server_error

Callback receives error=server_error or token exchange returns 500.

+
Cause

Something went wrong on Shopify's side. Usually transient.

Fix

Retry once after a short backoff (1–5s). If it persists for more than a few minutes, check status.shopify.com. Don't loop indefinitely — show the user an error and let them retry manually.

temporarily_unavailable

Callback receives error=temporarily_unavailable.

+
Cause

Shopify is rate-limiting or in maintenance. Sometimes also returned by upstream load balancers under load.

Fix

Back off and retry with exponential backoff capped at a few minutes. Surface a friendly retry-later message to the merchant.

Access token errors (using the issued token)

OAuth completed and you have a token in hand, but API calls fail.

401 Unauthorized / Invalid X-Shopify-Access-Token

Admin API calls return 401 with body { errors: '[API] Invalid API key or access token (unrecognized login or wrong password)' }.

+
Cause

Token has been revoked (merchant uninstalled), you're using the token against the wrong shop, you're sending it on the wrong header, or the token format is mangled.

Fix

Send the token only on the X-Shopify-Access-Token header, not Authorization: Bearer. Verify you're calling /admin/api/<version>/... on the same shop the token was issued for. If the merchant uninstalled, you must re-run OAuth on next install.

403 Forbidden

Admin API call returns 403 with a message about scope or permissions.

+
Cause

Token is valid but doesn't include the scope required for the resource you're calling. For example, calling /products.json with only read_orders.

Fix

Re-run the OAuth flow with the missing scope added to your scope list. Existing tokens never auto-expand — the merchant must re-approve.

402 Payment Required

Admin API returns 402.

+
Cause

The merchant's Shopify plan is frozen or overdue. Their store is in a paused/locked state.

Fix

There's nothing the app can do. Surface a friendly notice to the merchant pointing them to their Shopify billing.

invalid_token / token revoked

Admin API returns 401, and the merchant has uninstalled or reinstalled your app.

+
Cause

When a merchant uninstalls, all access tokens for that shop are immediately revoked. Reinstall issues a brand-new token — the old one stays revoked.

Fix

Listen for the app/uninstalled webhook to clean up stored tokens. On reinstall, capture the new token from the OAuth callback. Don't try to refresh — Shopify Admin API offline tokens don't have a refresh endpoint. See the rotation guide for the safe re-issue procedure.

423 Locked / shop_locked

API returns 423 or an error mentioning shop_locked.

+
Cause

The store is temporarily locked, often during a Shopify-initiated security review or a fraud check.

Fix

There's no client-side fix. The merchant has to resolve the lock with Shopify. Pause writes and surface the situation to the merchant.

General troubleshooting

Log the full error response. Shopify usually returns a JSON body with error and error_description fields. Don’t swallow them — log both.

Test against a development store. Spin up a dev store from your Partner Dashboard and exercise the OAuth flow there before going live. Errors against dev stores are easier to reset.

Check the Shopify status page. status.shopify.com shows partial outages that turn into spurious server_error and temporarily_unavailable responses.

Verify your URLs character-by-character. Most OAuth bugs are off-by-one in the redirect URI: a trailing slash, http vs https, or a different port.

Stop reusing authorization codes. The most common cause of invalid_grant is calling the token-exchange endpoint twice with the same code — often because of a retry-on-error helper. Always start a fresh OAuth flow instead of retrying the exchange.

Skip OAuth debugging entirely

If you just need a token for a one-off script or a third-party integration, generate one through the OAuth flow we already run. Paste your Client ID, Client Secret, pick scopes, approve on Shopify, copy your token.

Frequently asked questions

Why am I getting invalid_request from Shopify OAuth?+

invalid_request means a required OAuth parameter is missing or malformed. The most common causes are a missing client_id, an empty scope value, an invalid shop parameter (must be a real *.myshopify.com domain), or a redirect_uri that doesn't match what's registered in your app's settings exactly. Compare your authorization URL parameter-by-parameter against the working example in Shopify's docs.

What does invalid_grant mean in Shopify OAuth?+

invalid_grant means the authorization code returned in your callback can't be exchanged for an access token. Authorization codes are single-use and expire after about 10 minutes. The fix is almost always: don't reuse codes, exchange them server-side immediately, and don't retry the exchange after a failure — start the flow over with a fresh authorization request.

How do I fix HMAC verification failed in Shopify OAuth?+

Build the message to hash from the callback query parameters minus the hmac and signature parameters, sorted alphabetically by key, joined as key=value with & separators. Hash with HMAC-SHA256 using your client secret as the key. Compare to the hmac parameter as a hex string. Common mistakes: leaving hmac in the message, URL-encoding the message before hashing, using the wrong secret, or hashing the body instead of the query.

What does access_denied mean in a Shopify OAuth callback?+

access_denied means the merchant clicked Cancel on the consent screen instead of approving your app. There's no programmatic fix — you have to either reduce the scopes you're requesting (so the merchant trusts the install more) or guide them through the consent screen again with better in-app messaging.

Why does my Shopify access token return 401 Unauthorized?+

Either the token has been revoked (the merchant uninstalled your app), you're sending it on the wrong header (must be X-Shopify-Access-Token, not Authorization: Bearer), or the token belongs to a different shop than the one you're calling. Re-run the OAuth flow against the affected shop to get a fresh token.