Dev.to · 4 min read

New Shopify Admin API tokens expire after 24 hours: cache and refresh them in Node.js

New Shopify Admin API tokens expire after 24 hours: cache and refresh them in Node.js

If you built a Shopify integration before 2026, you probably clicked "Develop apps" in the store admin, copied a token that started with shpat_, and never thought about it again. That token didn't expire. New apps can't do that. Since January 1, 2026, custom apps are created in the Dev Dashboard, and a backend that talks to your own store gets its token from the client credentials grant. That token lasts 24 hours and comes with no refresh token. So the "paste it into .env once" habit breaks one day after deploy. Here's the request, and a small cache that keeps a valid token in hand. The token request You need the app's client ID and client secret (Dev Dashboard, your app, App settings, Credentials card), and the app has to be installed on the store. curl -X POST "https://your-store.myshopify.com/admin/oauth/access_token" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=client_credentials" \ -d "client_id=YOUR_CLIENT_ID" \ -d "client_secret=YOUR_CLIENT_SECRET" The response: { "access_token": "...", "scope": "read_products,write_orders", "expires_in": 86399 } Three details from Shopify's documentation that save debugging time: expires_in is always 86399 seconds. There is no refresh token. When the token runs out, you make the same request again. Don't send a scope field. Shopify ignores it. The token carries the scopes the store granted to the installation, and the scope field in the response tells you which. The body is form-encoded, not JSON. A cache that refreshes itself Asking for a new token on every API call works, but it doubles your requests. Shopify's own example caches the token and replaces it about a minute before it expires. This version does the same and adds one thing: if ten jobs start at the same moment with no token, they share one request instead of making ten. const SHOP = process.env.SHOPIFY_SHOP; // subdomain only, e.g. "your-store" const CLIENT_ID = process.env.SHOPIFY_CLIENT_ID; const CLIENT_SECRET = process.env.SHOPIFY_CLIENT_SECRET; let token = null; let tokenExpiresAt = 0; let pending = null; async function requestToken() { const response = await fetch(`https://${SHOP}.myshopify.com/admin/oauth/access_token`, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ grant_type: "client_credentials", client_id: CLIENT_ID, client_secret: CLIENT_SECRET, }), }); if (!response.ok) throw new Error(`Token request failed: ${response.status}`); const { access_token, expires_in } = await response.json(); token = access_token; tokenExpiresAt = Date.now() + expires_in * 1000; return token; } export async function getToken() { if (token && Date.now() < tokenExpiresAt - 60_000) return token; if (!pending) pending = requestToken().finally(() => (pending = null)); return pending; } Then every Admin API call asks the cache first: export async function adminGraphql(query, variables = {}) { const response = await fetch(`https://${SHOP}.myshopify.com/admin/api/2026-10/graphql.json`, { method: "POST", headers: { "Content-Type": "application/json", "X-Shopify-Access-Token": await getToken(), }, body: JSON.stringify({ query, variables }), }); if (!response.ok) throw new Error(`GraphQL request failed: ${response.status}`); const { data, errors } = await response.json(); if (errors?.length) throw new Error(`GraphQL errors: ${JSON.stringify(errors)}`); return data; } Read expires_in from the response instead of hard-coding 24 hours. 2026-10 is the latest stable API version as I write this. Serverless and multiple workers A module-level variable only helps one long-running process. Serverless functions start cold, and five workers means five caches and five tokens. For those setups, keep the token and its expiry time in a shared store such as Redis, with the key's TTL set a minute shorter than expires_in. Every worker reads the key first and only requests a token when it's missing. Three errors you'll probably meet shop_not_permitted. The full message is "Oauth error shop_not_permitted: Client credentials cannot be performed on this shop." This grant only works when the app and the store are in the same Shopify organization. Installing the app on a client's store isn't enough. Either create the app inside the client's organization, or use the authorization code grant. "Invalid API key or access token". Usually the client secret was sent in the X-Shopify-Access-Token header. Exchange the credentials for a token first. A broken URL. The code adds .myshopify.com itself. If SHOPIFY_SHOP holds the full domain, the request goes nowhere. When this grant is the wrong one Client credentials fits one case: your backend, your organization's store. An app that other merchants install needs the authorization code grant, and an app embedded in the Shopify admin uses token exchange, which the Shopify CLI template handles for you. I wrote up all of the methods, the Dev Dashboard steps and the token lifetimes for each one here: Shopify Admin API access token: how to get one in 2026.

This is a summary aggregated from Dev.to. Read the complete article on the original site:

Read full article at Dev.to

More Programming & Dev News