Handling Shopify expiring offline access tokens
Starting April 1, 2026, Shopify is requiring all new public apps to use expiring offline access tokens. If you're building a new app, this changes how your app authenticates with Shopify in the background.
What's changing
Offline access tokens now expire after 60 minutes. Shopify added refresh support so apps can obtain new tokens using a refresh token (valid for 90 days). This brings Shopify in line with modern OAuth and limits exposure if a token leaks.
Who's affected
| App type | Affected? |
|---|---|
| New public apps created on or after April 1, 2026 | Yes, required |
| Existing public apps created before April 1, 2026 | No |
Online access tokens (per-user, for embedded admin sessions) are unchanged.
How the new flow works
Include expiring=1 when exchanging a session token or auth code. Shopify returns both an access token and a refresh token. Store both with the expiration timestamp. When the access token is close to expiring, exchange the refresh token for a new pair, and replace your stored values with the new ones.
Best practices
- Centralize refresh. Route every Shopify API call through one helper that checks expiry, refreshes if needed, and retries on 401. Don't scatter refresh logic across handlers, webhooks, and jobs.
- Refresh proactively. Refresh a few minutes before expiry (not on 401), so active requests always have a valid token.
- Lock around refresh. Use a per-shop mutex so concurrent requests don't fire two refreshes and invalidate each other.
- Persist atomically. Save the new access token, refresh token, and expiration in a single transaction. A partial write leaves you unable to refresh.
- Watch for
invalid_grant. That means the refresh token is no longer valid (90-day window passed, app reinstalled, or revoked). The merchant needs to re-authorize. - Keep the chain warm. If a shop is idle for 90+ days the refresh token expires. Refresh at least every couple of weeks to avoid losing access on quiet shops.
Cron jobs and scheduled work
Background jobs are the highest-risk spot for token expiry. Recommended patterns:
- Check token validity per-shop at the start of each iteration. Don't trust a token cached hours ago.
- Isolate failures per shop. If one refresh fails with
invalid_grant, log it and continue. Don't let one bad token kill the batch. - Stagger refresh calls if you're processing many shops at once.
App Proxies
App Proxy requests are authenticated by HMAC, not by access token. But anything the proxy handler does against the Admin API still needs a valid offline token. Verify the HMAC first, then route through your normal refresh helper. Proactive background refresh keeps the proxy hot path fast.
Common pitfalls
- Forgetting to update the refresh token. Each refresh returns a new one. Save it.
- Clock skew. Refresh when expiring in less than 5 min, not less than 0 min.
- Caching tokens across pods. Cache in a shared store (Redis, your DB) so refreshes don't fight each other.
Using Identify with expiring tokens
Mantle's /identify endpoint (see also the Identifying customers guide) takes the Shopify offline access token as accessToken. Mantle stores it and uses it to make Shopify API calls on the merchant's behalf for billing and subscription operations.
With expiring tokens:
- Re-call Identify whenever you refresh. Pair your refresh path with an Identify call so Mantle's stored copy stays current. Passing a fresh token on every call is the safest pattern.
- Mantle does not refresh on your behalf. You handle the Shopify OAuth refresh; Mantle just needs the current valid token.
- Stale Mantle tokens surface as billing-side errors. If charges or subscription updates start failing, re-Identify the affected merchants.