Skip to content

Auth & sessions ​

Auth is the topic that confuses everyone at first. That's because there are three different tokens. Learn which is which, and the rest follows.

The three tokens ​

TokenWho issues itLivesUsed for
Session token (JWT)App Bridge, in the browser~1 minuteProves to your server which shop and user is making this request
Access tokenShopify, via token exchange or OAuthOffline: 1 h (expiring) + refresh token. Online: tied to the user's sessionLets your server call the Admin API
Refresh tokenShopify, with an expiring offline token90 days, rotates on every refreshGets a new offline access token without the merchant

Modern flow: managed install + token exchange ​

The old OAuth redirect dance is no longer the default for embedded apps:

  1. Shopify handles install. Scopes come from shopify.app.toml. That is managed installation.
  2. Your app gets session tokens from App Bridge on every request.
  3. Your server exchanges the session token for an access token in a single server-to-server call. That is token exchange.

With the React Router template, all of this is one line: await authenticate.admin(request).

Online vs offline access tokens ​

OfflineOnline
Tied toThe shopOne staff member's login
Use forBackground jobs, webhooks, cronActions that must respect the user's permissions
Default in template✅Opt-in (useOnlineTokens: true)

Most apps only need offline.

⚠️ Expiring offline tokens (new rule) ​

Required for public apps

  • Public apps created on or after 2026-04-01 must use expiring offline access tokens.
  • Public apps created before that must migrate by 2027-01-01.
  • The access token lasts 1 hour. The refresh token lasts 90 days and rotates: each refresh returns a new one, and you must store it.
  • You request the new type with expiring=1 on the token exchange call.
  • Custom and merchant-created apps are exempt.

If you use the official libraries, refreshing is handled for you. If you write your own background workers (like a FlowFlex Moleculer service calling Shopify), you must refresh before calling the API and persist the new refresh token.

Source: Expiring offline access tokens

Calling Shopify from outside the app (e.g. a worker) ​

ts
import shopify from "~/shopify.server";

// in a job: load the stored offline session for the shop
const { admin } = await shopify.unauthenticated.admin("my-shop.myshopify.com");
await admin.graphql(`{ shop { name } }`);

unauthenticated.admin doesn't check a request. It loads the stored offline session, so only call it from trusted server code.

Classic OAuth (authorization code grant) ​

You still need this for non-embedded apps and for some external integrations. Shopify redirects the merchant to /admin/oauth/authorize, then back to your callback with a code. You exchange the code for a token and verify the hmac and state on the callback. The React Router package doesn't support non-embedded apps, so use the plain @shopify/shopify-api library for that.

Check yourself ​

  1. Why can't you use a session token to call the Admin API directly?
  2. Your cron job gets 401 at 3 a.m. What likely happened with expiring tokens?
  3. When would you need an online token?