Authenticate an embedded app without a template
Embedded Shopify apps get access tokens through token exchange: App Bridge provides a short-lived ID token proving the merchant's current session, and your backend exchanges it with Shopify for an access token to call Shopify APIs. For the endpoint and parameters behind this flow, see the access tokens reference.
When you scaffold your app with Shopify CLI, the Shopify app template handles this automatically. Use this tutorial if you're building an embedded app with a custom frontend that doesn't call authenticate.admin(), or if you need to understand what the template does under the hood.
For server-side integrations acting on stores in your own Shopify organization, use the client credentials grant instead. To learn how authentication works for other common approaches, see About app authentication.
For server-side integrations acting on stores in your own Shopify organization, use the client credentials grant instead. To learn how authentication works for other common approaches, see About app authentication.
Anchor to What you'll learnWhat you'll learn
In this tutorial, you'll learn how to do the following tasks:
- Get an ID token from App Bridge
- Exchange it for an offline or online access token
- Make authenticated GraphQL Admin API requests
- Refresh an expiring offline token
Your backend language selection applies to the backend steps. The first step runs in the browser, so its samples are always JavaScript.
Requirements
You have your app's client ID and client secret from the Dev Dashboard.
You've configured the access scopes your app needs.
You've installed your app on a dev store, which grants the access scopes it requests.
Your frontend loads App Bridge from the CDN script tag, so the shopify global is available to call.
Project
Anchor to Get an ID token from App BridgeGet an ID token from App Bridge
An ID token is a short-lived JWT that Shopify generates when a merchant opens your app. It proves the request is from an authenticated Shopify user. App Bridge generates a fresh token for each session and your frontend sends it to your backend on every request.
Anchor to Call idToken() in your frontendCall id Token() in your frontend
The method you use depends on where your app is rendered:
- In App Home, your app's main page in the Shopify admin, call
shopify.idToken(). - In an admin UI extension, which adds functionality to an admin resource page such as Products or Orders, call
auth.idToken().
ID tokens expire after one minute. Fetch a fresh one on each request rather than caching it.
Anchor to Send the token to your backendSend the token to your backend
Include the ID token in the Authorization header on requests from your frontend to your backend.
Anchor to Exchange the ID token for an access tokenExchange the ID token for an access token
Your backend validates the ID token, then sends it to Shopify's token endpoint to receive an access token. Shopify supports two access token types. Offline access tokens are the default: they persist across sessions and aren't tied to a specific user, which suits background jobs, webhooks, and scheduled work. Online access tokens are tied to the staff member who opened your app and expire with their session, so use them when your app needs to enforce per-user permissions or attribute actions to a specific person. For a fuller comparison, see Access token types.
Anchor to Validate the ID tokenValidate the ID token
Validate the ID token before your app trusts it. The samples use a JWT library for this: jsonwebtoken for Node.js and PyJWT for Python. Check the signature against your client secret, then verify the following claims:
| Claim | What to check |
|---|---|
exp | Must be in the future. |
nbf | Must be in the past. |
aud | Must match your app's client ID. |
iss and dest | Hostnames must match. |
If any check fails, the library raises an error and the exchange routes return a 401 before reaching Shopify's token endpoint.
When you reject a request with a 401, set the X-Shopify-Retry-Invalid-Session-Request header if the request came from your frontend over XHR or fetch. App Bridge intercepts the response, fetches a fresh ID token, and retries the request once.
Anchor to Request an offline access tokenRequest an offline access token
Offline access tokens persist across sessions. Use them for background jobs, webhooks, and anything that runs without an active merchant session. New public apps must use expiring offline access tokens. Include expiring=1 in your request and Shopify returns a refresh token alongside your access token. If you're updating an existing public app that still requests non-expiring tokens, see Migrate to expiring offline access tokens.
If the ID token is expired or otherwise invalid, Shopify returns a 400 Bad Request. An ID token lives about a minute, so this is a routine condition rather than a server fault, and a fresh ID token fixes it. Handle it the way you handle a failed validation check: respond with a 401 and the X-Shopify-Retry-Invalid-Session-Request header, so App Bridge fetches a new ID token and retries. Reserve 5xx responses for failures that a fresh ID token can't fix, such as rejected client credentials.
Anchor to Handle the offline token responseHandle the offline token response
Store the access_token and refresh_token securely on your backend, along with expires_in and refresh_token_expires_in so you know when each one expires. Never expose them to the browser.
Response fields
| Field | Description |
|---|---|
access_token | The token to include in API requests. |
scope | The access scopes granted to your app. The exchange doesn't ask for scopes, so this is a readback of what you configured for your app. To add or change them, see Manage access scopes. |
expires_in | Seconds until the token expires. |
refresh_token | Used to get a new access token before expiry. |
refresh_token_expires_in | Seconds until the refresh token expires (90 days). |
Anchor to (Optional) Request an online access token(Optional) Request an online access token
Online access tokens are scoped to the current user and session. Use them if your app needs to enforce per-user permissions or attribute actions to a specific staff member. Skip this step if offline access tokens meet your needs. For more about online tokens, including their lifetime and the associated_user fields, see Online access tokens.
Anchor to Exchange the ID token for an online tokenExchange the ID token for an online token
Each online token is valid for 24 hours and tied to the staff member who opened your app. As with the offline exchange, a 400 Bad Request means the ID token is stale, so answer it with a 401 and the retry header.
Anchor to Handle the online token responseHandle the online token response
The associated_user_scope field contains the intersection of your app's scopes and that user's permissions. Use it to enforce what the current staff member can do.
Response fields
| Field | Description |
|---|---|
access_token | The token to include in API requests. Valid for 24 hours. |
scope | The access scopes granted to your app. |
expires_in | Seconds until the token expires. |
associated_user_scope | The intersection of your app's scopes and the user's permissions. |
associated_user | The authenticated user. Only trust email if email_verified is true. |
Anchor to Make authenticated API requestsMake authenticated API requests
Include the access token in the X-Shopify-Access-Token header on all GraphQL Admin API requests.
Anchor to Add the access token headerAdd the access token header
Include the access token in every request. If the access token expires or Shopify rejects it during an active merchant session, re-run token exchange with the ID token from the current request. For background jobs with no active session, use the refresh_token instead. See Refresh an expiring offline token.
Anchor to Refresh an expiring offline tokenRefresh an expiring offline token
Use this flow when your access token expires and no merchant session is available, such as for webhooks, scheduled jobs, and other background work. Without an active session, you can't get a fresh ID token from App Bridge, so you use the stored refresh_token instead.
Store the refresh_token from the exchange response and use it before expires_in seconds elapse. Refresh tokens are one-time-use and expire after 90 days.
Background refreshes usually run in-process from your scheduler or job runner, so there's no endpoint to secure. If you do expose one over HTTP, protect it with your own internal credential, such as a shared secret.
Anchor to Send the refresh requestSend the refresh request
| Parameter | Description |
|---|---|
client_idrequired | The client ID for the app. |
client_secretrequired | The client secret for the app. |
grant_typerequired | The value refresh_token indicates that a refresh token grant is being used. |
refresh_tokenrequired | The refresh token received when the access token was issued. |
Shopify returns a new access token and a new refresh token. A few things to keep in mind:
- Each refresh issues a new refresh token with a new 90-day expiration. Store it and discard the old one.
- The previous access token stays valid until its
expires_induration ends, but use the new token for all new requests. - Token refreshes are resilient to ambiguous failures. If a refresh request times out, hits a network error, or returns a transient
5xx, retry promptly with the samerefresh_token. The repeated request returns the same rotated credentials rather than issuing another set. Treat a successful rotation as consuming the previous refresh token, and don't rely on a fixed retry-window duration.
When a refresh token can no longer be used, Shopify returns 401 Unauthorized with {"error": "invalid_request"} and the description This request requires an active refresh_token. Shopify returns this same response for every terminal case, including an unknown token, a token replayed after the retry window, an expired token, and a revoked or uninstalled app, so don't branch on which one it was.
Treat that 401 as final: stop retrying, and re-authenticate by running token exchange the next time a merchant opens your app. Transient failures, such as network errors, timeouts, 5xx responses, and 429 responses, are safe to retry with the same refresh_token.
Refreshing an access token doesn't change your client secret. Rotating your client secret is a separate operation that invalidates tokens tied to the old secret and requires migrating every store. See Rotate your client secret.
Anchor to Tutorial complete!Tutorial complete!
Your embedded app now exchanges App Bridge ID tokens for access tokens and refreshes them before they expire.
Anchor to Next stepsNext steps
Use your access token to query and mutate store data with the GraphQL Admin API.
Set up webhooksRegister webhooks for background tasks that run using your offline access token.
Manage access scopesChange your declared scopes after launch, and request or revoke optional scopes dynamically.
Delegate API accessGive subsystems scoped, limited access to Shopify APIs without sharing your app's full credentials.