Authenticate a standalone or API-only app
Standalone and API-only apps run outside the Shopify admin, unlike embedded apps. A standalone app serves its own UI, and an API-only app has no UI and runs as a server-side integration. Because they can't use ID tokens, they authenticate using the OAuth authorization code grant flow: redirect the merchant to Shopify, get their approval, and exchange a code for an access token.
If your app only acts on stores in your own Shopify organization, use the client credentials grant instead. It skips the merchant authorization flow. To learn how authentication works for other common approaches, see About app authentication.
If your app only acts on stores in your own Shopify organization, use the client credentials grant instead. It skips the merchant authorization flow. 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:
- Redirect a merchant to Shopify's authorization page
- Validate the callback Shopify sends back to your app
- Exchange the authorization code for an offline access token
- Make authenticated GraphQL Admin API requests
Requirements
You have your app's client ID and client secret from the Dev Dashboard.
You've added your app's redirect URI to your app's settings in the Dev Dashboard, or to the [auth] section of your shopify.app.toml.
You know which access scopes your app requires.
Project
Send the merchant to Shopify to approve the access your app is asking for. Shopify presents a permissions prompt, and the merchant approves or denies it.
Construct the authorization URL and redirect the merchant to it. Include a randomly generated state value (nonce) and store it so you can verify it in the next step. This prevents CSRF attacks.
| Parameter | Description |
|---|---|
client_id | Your app's client ID. |
scope | A comma-separated list of access scopes your app needs. Some scopes grant access to protected customer data, which places additional requirements on your app. |
redirect_uri | The URL Shopify redirects to after authorization. Must exactly match a redirect URI you've configured in the Dev Dashboard or your app's configuration TOML file. |
state | A randomly generated nonce unique to this request. |
Anchor to Validate the callbackValidate the callback
After the merchant approves your app, Shopify redirects to your redirect_uri with an authorization code and several query parameters. Validate all of them before exchanging the code.
Anchor to Verify the ,[object Object], parameterVerify the state parameter
state parameterCompare the state parameter to the nonce you stored in step 1. If they don't match, reject the request.
Anchor to Verify the HMACVerify the HMAC
Remove the hmac parameter from the query string, sort the remaining parameters alphabetically, and compute an HMAC-SHA256 hash using your client secret. The result must match the hmac value. Use a constant-time comparison, such as crypto.timingSafeEqual in Node.js or hmac.compare_digest in Python, so the check doesn't leak timing information.
If you're using one of Shopify's API libraries, these checks are built in:
Anchor to Validate the ,[object Object], domainValidate the shop domain
shop domainConfirm the shop parameter matches ^[a-zA-Z0-9][a-zA-Z0-9\-]*\.myshopify\.com$. Anchor the pattern at both ends. Without the trailing $, a value such as exampleshop.myshopify.com.attacker.example passes the check, and your app sends the authorization code to a host you don't control.
Anchor to Exchange the code for an offline access tokenExchange the code for an offline access token
Offline access tokens persist across sessions and aren't tied to a specific staff member. Exchange the authorization code for one by sending a POST request to Shopify's token endpoint.
Anchor to Send the token requestSend the token request
New public apps must use expiring offline access tokens. Include expiring=1 in your request. 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.
A successful exchange returns the token and its metadata:
Response fields
| Field | Description |
|---|---|
access_token | The token your app uses to make authenticated API requests. Store it securely. |
scope | The access scopes that were granted. |
expires_in | Seconds until the access token expires. |
refresh_token | Used to get a new access token when the current one expires. |
refresh_token_expires_in | Seconds until the refresh token expires (90 days). |
Anchor to Confirm the granted scopesConfirm the granted scopes
Check that scope in the response includes all the scopes your app requires. Merchants can modify requested scopes during authorization, so always verify before storing the token. A write_* scope includes its matching read_* scope, so when you check the granted scope list, look for the write scope you requested rather than a separate read scope.
To request additional scopes later, redirect the merchant to the authorization URL again with the updated scope list. Shopify prompts them to approve the added permissions. Apps built with Shopify CLI update scopes through managed installation instead. See Authentication for apps built with Shopify CLI.
Anchor to Store the tokensStore the tokens
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 token expires.
Anchor to (Optional) Request an online access token(Optional) Request an online access token
Online access tokens are scoped to the staff member who authorized them, and they expire with that user's session. Request one when your app needs to respect an individual user's permission level or attribute actions to a specific user. Skip this step if an offline token is all your app needs.
A single authorization request returns one token type, so this step changes the flow you just built rather than adding to it. Your app can hold both, but each one needs its own record: key the offline token by shop, and each online token by shop and staff member. An online access token has no refresh token, so writing one into the offline token's record ends your app's background refresh. The samples on this page keep a single record per shop, so add a second store before you run this flow.
Append &grant_options[]=per-user to the authorization URL you built in Redirect to Shopify's authorization page, then run the flow again. The code exchange returns an online access token instead of an offline one, and the response adds associated_user and associated_user_scope.
For what those fields contain, how long the token lasts, what a user logging out revokes, and why a valid token can still return 403, see Online access tokens.
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. When it nears expiry, use the stored refresh_token to get a new one rather than sending the merchant back through authorization. See Refresh the access token.
Anchor to Refresh the access tokenRefresh the access token
Expiring offline access tokens come with a refresh_token. Before the access token expires, exchange the refresh token for a new access token so your app keeps working without sending the merchant back through authorization.
Anchor to Exchange the refresh tokenExchange the refresh token
Send the stored refresh_token to the token endpoint before expires_in seconds elapse. The response contains a new access token and a new refresh token, each with updated expiry times, so store both and discard the old pair.
Request
Response
Anchor to Handle refresh failuresHandle refresh failures
Treat a successful rotation as consuming the previous offline refresh 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 same refresh_token, and the repeated request returns the same rotated credentials rather than issuing another set. Don't rely on a fixed retry-window duration. If the refresh token is expired or otherwise invalid, Shopify returns 401 Unauthorized with {"error": "invalid_request"}. Clear the stored token pair and send the merchant back through authorization to mint a new one. For the full error behavior, see Refresh an expiring offline token.
Anchor to Tutorial complete!Tutorial complete!
Your app now completes the authorization code grant and holds an access token it can use against Shopify APIs.
Anchor to Next stepsNext steps
Find your client ID and secret, secure them, and rotate your client secret.
Query store dataUse 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.