OAuth issues a valid token for a deactivated store, but all Admin API calls return 404 — app shows false "connected"

Hello,

We’re the developers of the Synder app (Shopify app client id 4b7c5c9c…, Partner organization 1572206). We’ve hit a confusing and reproducible situation around store reconnection and would like help understanding the expected

behavior.

Summary of the problem

When a merchant reconnects a store that has been deactivated/closed on Shopify, the OAuth flow still completes successfully and Shopify hands us a valid access token. But every subsequent Admin API request made with that brand-new

token returns HTTP 404 {“errors”:“Not Found”} — including endpoints that require no scope. The result is that, from the app’s point of view, the connection “succeeded,” yet we can’t read any data at all. This makes the app (and the

merchant) believe the store is connected when in reality it is not usable.

The two sides that contradict each other

  1. Connect side (looks fine): POST /admin/oauth/access_token returns HTTP 200 with a valid access_token and the full list of granted scopes.

  2. Data side (completely broken): Immediately after, using that same fresh token, every Admin API call returns HTTP 404 {“errors”:“Not Found”}.

Stores affected

(Both belong to the same merchant. Same behavior on both.)

What we observed, step by step

  1. Merchant initiates reconnect. Shopify OAuth redirect + token exchange complete normally.

  2. POST https://going-steady-ca.myshopify.com/admin/oauth/access_token → HTTP 200, returns a token (prefix shpat_2f8dee…) and scopes (read_orders, read_products, read_shopify_payments_payouts, etc.).

  3. Using that token immediately:

  • GET /admin/api/2026-01/shop.json → HTTP 404 {“errors”:“Not Found”}

  • GET /admin/api/2026-01/webhooks.json → HTTP 404 {“errors”:“Not Found”}

  • POST /admin/api/2026-01/graphql.json → HTTP 404 {“errors”:“Not Found”}

  • GET /admin/oauth/access_scopes.json (no API version, no scope required) → HTTP 404 {“errors”:“Not Found”}

  1. We retried shop.json across multiple API versions — 2026-01, 2025-10, 2025-07 — all return HTTP 404.

  2. Response headers on the 404s show the request is reaching Shopify and is attributed to our app: x-shopify-api-version: 2026-01, x-stats-apiclientid: 3879321, x-stats-apipermissionid populated.

What we eventually found via the Partner API

Querying our app’s events for the store, the most recent relationship event is:

  • RELATIONSHIP_DEACTIVATED at 2025-08-07T17:19:02Z

So the store has been deactivated/closed since 2025-08-07. That explains the 404s — but it does not explain why OAuth still mints a working-looking token for a deactivated store.

Why this is a problem for us

Because the token grant succeeds, our reconnect flow has no reliable real-time signal that the store is actually dead. The app marks the company as “connected,” and only later (when background syncing fails with 404s) do we realize

the store is unreachable. Merchants then repeat the reconnect, get “success” again, and contact our support confused.

Our questions

  1. Is it expected that POST /admin/oauth/access_token returns a valid token for a deactivated/closed store? If so, why?

  2. Why does even /admin/oauth/access_scopes.json (no version, no scope) return 404 “Not Found” rather than a clear “store unavailable / not found” status that we could detect at connect time?

  3. Is there a real-time, Admin-API-side signal (a specific status code, header, or endpoint) we can use during the OAuth/connect flow to detect a deactivated store — without polling the Partner API afterward?

  4. For deactivated stores, would it be possible for the API to return a distinct, documented response (e.g. 402/423 with a clear body) instead of a generic 404 “Not Found”, so apps can tell “store closed” apart from “app uninstalled”

or “wrong token”?

Request IDs for tracing (all 2026-06-15)

  • access_token (HTTP 200): b8c66e27-1a80-429f-9e32-291531a374fc-1781528311

  • shop.json (HTTP 404), CA: e337b102-f712-4449-8b8d-b1a8b7ac1df4-1781528312

  • webhooks.json (HTTP 404), CA: edff6d9c-5352-4cb3-9ead-45bf045b667d-1781528312

  • shop.json (HTTP 404), USA: 45836504-938a-4441-becc-69c442a0a5e1-1781528321

Thank you — any guidance on the expected behavior and a recommended detection approach would help us give merchants accurate connection status.

Hi @Alexey_Sergeev! Good write-up, and your read on it is basically right. A successful token exchange against a deactivated store is expected behavior. The OAuth layer that mints the token and the Admin API data layer that serves shop data are separate systems. When a store is deactivated its subscription is cancelled, the admin becomes inaccessible, and all apps are automatically uninstalled, so the data layer stops serving that shop. The token request can still validate and return a token, but every subsequent call has nothing to read from, which is why access scopes 404s along with everything else. The token grant on its own tells you nothing about whether the store is reachable.

On the status codes, the response codes reference is the documented mapping for store states. A frozen store with an unpaid balance returns 402, a store marked as fraudulent returns 403, and a locked store (repeated rate-limit overages or fraud risk) returns 423. A deactivated or closed store has no distinct code today, it just falls under the generic 404 Not Found. That also means a 404 by itself isn’t a deactivation signal, since a wrong shop handle or a deleted resource produces the same {"errors":"Not Found"} response.

For detecting this at connect time, the pattern I’d suggest is to make one lightweight read right after the token exchange (a minimal shop { name } GraphQL query, or shop.json) and use that as your connection check. Treat a 200 as connected, and treat the unavailable family (401, 402, 403, 404, 423) as do not mark connected, surfacing an error and prompting a retry rather than trusting the token grant. For authoritative lifecycle state, the Partner API relationship events plus the app/uninstalled and shop/redact webhooks are the explicit signals (which is what you already used to confirm the deactivation). One thing to keep in mind there is that the events query is a historical log rather than a live status field, so order by when each event occurred and act on the latest one, since a store can be deactivated and later reactivated.

A dedicated status code or field for deactivated/closed stores is a reasonable ask, and it’s useful feedback for the team to consider. I can’t commit to whether or when that would change, but the practical setup above (quick Admin API read for connectivity, Partner API events for the authoritative reason) should let you handle these stores cleanly in the meantime.

I hope this helps!