Skip to main content

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.

Info

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.

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

App credentials

You have your app's client ID and client secret from the Dev Dashboard.

Redirect URI

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.

Access scopes

You know which access scopes your app requires.

Project

Anchor to Redirect to Shopify's authorization pageRedirect to Shopify's authorization page

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.

Anchor to Build the authorization URLBuild the authorization URL

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.

ParameterDescription
client_idYour app's client ID.
scopeA 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_uriThe 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.
stateA 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.

https://your-app.example.com/callback?code={authorization_code}&hmac={hmac}&shop={shop}&state={nonce}&timestamp={timestamp}

Compare the state parameter to the nonce you stored in step 1. If they don't match, reject the request.

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:

Confirm 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:

{
"access_token": "f85632530bf277ec9ac6f649fc327f17",
"scope": "read_products,write_orders",
"expires_in": 3600,
"refresh_token": "shprt_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"refresh_token_expires_in": 7776000
}
Response fields
FieldDescription
access_tokenThe token your app uses to make authenticated API requests. Store it securely.
scopeThe access scopes that were granted.
expires_inSeconds until the access token expires.
refresh_tokenUsed to get a new access token when the current one expires.
refresh_token_expires_inSeconds 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.

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.

Anchor to Add the per-user parameter to the authorization URLAdd the per-user parameter to the authorization URL

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.

https://{shop}/admin/oauth/authorize?client_id={client_id}&scope={scopes}&redirect_uri={redirect_uri}&state={nonce}&grant_options[]=per-user

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

curl -X POST \
https://{shop}.myshopify.com/admin/oauth/access_token \
-H 'Content-Type: application/x-www-form-urlencoded' \
-H 'Accept: application/json' \
-d 'client_id={client_id}' \
-d 'client_secret={client_secret}' \
-d 'grant_type=refresh_token' \
-d 'refresh_token={refresh_token}'

Response

{
"access_token": "shpat_yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy",
"expires_in": 3600,
"refresh_token": "shprt_zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz",
"refresh_token_expires_in": 7776000,
"scope": "write_products,read_orders"
}

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.

Your app now completes the authorization code grant and holds an access token it can use against Shopify APIs.

Was this page helpful?