Skip to main content

Manage access scopes

Access scopes control what your app can read and write. This guide covers where to declare scopes, whether to make them required or optional, and how to change them after your app is installed. For the full list of scopes, see the access scopes reference. For how your app gets the access token that carries these scopes, see About app authentication.


Anchor to Where you configure scopesWhere you configure scopes

Where you declare your app's scopes depends on how your app is set up:

How your app is set upWhere you declare scopes
Built with Shopify CLIThe [access_scopes] section of your app's configuration TOML file. Run shopify app deploy to deploy the scopes you've modified.
Created in the Dev DashboardYour app's version in the Dev Dashboard. Release the version to deploy the scopes you've modified.
Implements the authorization code grant itselfThe scope parameter of the authorization URL your app builds.

However you declare them, your app can only use a scope once it's been approved on the store. If your app acts only on stores in your own organization, you approve that change yourself when you release the version, so there's no merchant consent step.


Most apps start with a small set of scopes. The most frequently used GraphQL Admin API scopes are read_products, read_orders, read_customers, read_inventory, read_draft_orders, read_discounts, read_content, and read_themes, each with a write_ counterpart.

To find the scope a specific call needs, look up the field or mutation in the GraphQL Admin API reference. For the full list of scopes, including unauthenticated and customer scopes, see the access scopes reference.

Three things to know before you declare scopes:

  • A write scope includes read. write_products grants read_products, so declare the write scope on its own when your app needs both. Declaring the read scope as optional while the write scope is required fails to deploy. See Moving a scope between scopes and optional_scopes.
  • Orders are limited to the last 60 days. read_orders and write_orders cover that window. To reach older orders, request read_all_orders and declare it alongside them.
  • More scopes reach protected customer data than you'd expect. Customer scopes are the obvious case, but orders, draft orders, fulfillments, abandoned checkouts, and store comments all carry customer data too. You can declare any of them, but outside of dev stores the API redacts protected fields until your app meets the protected customer data requirements and is approved.

None of these choices are permanent. You can change which scopes you declare, and whether each one is required or optional, after your app is installed. See Modify declared scopes.


Anchor to Access scope configurationsAccess scope configurations

There are two ways you can configure your access scopes: scopes, which merchants grant when they install your app, and optional_scopes, which your app requests later and merchants can decline.

Required access scopes. Merchants must grant these when they install your app, so your app is guaranteed to have them after installation.

Define them in the scopes field of your app's TOML file:

# shopify.app.config-name.toml
name = "Example App"
client_id = "a61950a2cbd5f32876b0b55587ec7a27"
application_url = "https://www.app.example.com/"
embedded = true

[access_scopes]
scopes = "read_discounts,write_products"

Scopes your app requests after installation. Merchants can grant or decline them, and can revoke ones they've granted. Use optional scopes to offer features to some stores without requiring the same data access from every install.

Define scopes that your app can request dynamically in the optional_scopes field of your app's TOML file:

# shopify.app.config-name.toml
name = "Example App"
client_id = "a61950a2cbd5f32876b0b55587ec7a27"
application_url = "https://www.app.example.com/"
embedded = true

[access_scopes]
scopes = "" # The `scopes` field is still necessary, but can be empty.
optional_scopes = ["read_discounts", "write_products"]

Your app requests these scopes after installation completes, if it needs them.


Anchor to Modify declared scopesModify declared scopes

To add or remove access scopes for your app, update your app's configuration TOML file and deploy the changes.

  1. Modify the scopes or optional_scopes fields in your app's TOML file to include the access scopes you want.

  2. Deploy the changes by running the following Shopify CLI command:

    Terminal

    shopify app deploy
  3. Optionally, subscribe to the app/scopes_update topic to receive webhooks when the granted scopes change.

If your app doesn't use Shopify CLI, the step you take instead depends on how it's set up. For an app created in the Dev Dashboard, release a new version with the scopes you want. For an app that implements the authorization code grant itself, send the merchant through the authorization URL again with the updated scope list. In every case, merchants are prompted to approve scopes you add.

If you modify the scopes field, then the following happens:

  • Merchants are prompted to approve the updated access scopes when they open your app. The app/scopes_update webhook fires when the merchant approves the changes.
  • If the change reduces scopes, the merchant isn't prompted and your app loses access to those scopes automatically when the merchant opens the app. The app/scopes_update webhook fires when the merchant opens the app.

Anchor to Modifying the ,[object Object], fieldModifying the optional_scopes field

If you modify the optional_scopes field, then the following happens:

  • Your app can start requesting the new access scopes.
  • The granted scopes for your app installation don't change until your app requests the new scopes dynamically and the merchant grants access. The app/scopes_update webhook fires when the merchant approves the changes.

Anchor to Moving a scope between ,[object Object], and ,[object Object]Moving a scope between scopes and optional_scopes

You can change whether a scope is required or optional by moving its handle between the scopes and optional_scopes fields, then deploying the change.

If you move a scope from scopes to optional_scopes, then the following happens:

  • Stores that already granted the scope while it was required keep it. The scope is retained as an approved optional scope, so the merchant isn't re-prompted and the scope isn't revoked when they open your app. You don't need to request the scope again for existing installations.
  • New installations don't receive the scope until your app requests it dynamically, the same as any other optional scope.

A required scope can't implicitly grant an optional one. For example, write_products grants read_products, so if you declare read_products as optional while write_products is still required, then deploying fails with the following error:

Declared optional_scopes [read_products] cannot be implicit required scopes.

To resolve this, remove the implicitly granted scope from optional_scopes. In this example, you'd remove read_products, because write_products already grants it. If you want read_products to genuinely be optional, then move or remove write_products instead.


Anchor to Query currently granted scopesQuery currently granted scopes

Use a GraphQL Admin API query to get the currently granted access scopes for your app installation.

Example request to retrieve currently granted access scopes using currentAppInstallation

POST https://{shop}.myshopify.com/admin/api/{api_version}/graphql.json

query {
currentAppInstallation {
accessScopes {
description
handle
}
}
}

Response

{
"data": {
"currentAppInstallation": {
"accessScopes": [
{
"description": "Modify products, variants, and collections",
"handle": "write_products"
},
{
"description": "Read products, variants, and collections",
"handle": "read_products"
}
]
}
}
}

Shopify's API libraries wrap this query in a helper method:

LibraryMethod
App Bridge APIshopify.scopes.query()
React Router APIscopes.query()

Anchor to Request new access scopes dynamicallyRequest new access scopes dynamically

Note

You can only request additional access scopes dynamically if they're configured as optional_scopes in your app's TOML file, and the configuration changes have been deployed. Access scopes configured in the scopes field can't be requested dynamically.

Requesting a scope asks the merchant to grant or decline it. What the merchant sees depends on the mechanism: the App Bridge method opens a permission grant modal on top of your running app, while the request URL and the React Router method redirect the browser to the Shopify admin.

Optional scopes request grant modal

If you're not using one of Shopify's API libraries, request access scopes by directing the merchant to the request URL:

Browser redirect URL

https://admin.shopify.com/store/{STORE_NAME}/oauth/install?client_id={CLIENT_ID}&optional_scopes={REQUESTED_SCOPES}
Query parameterDescription
STORE_NAMEThe name of the merchant's store.
CLIENT_IDThe app's client ID.
REQUESTED_SCOPESA comma-separated list of access scopes to request. This must be a subset of the declared optional_scopes in your TOML file.

For example:

https://admin.shopify.com/store/my-cool-store/oauth/install?client_id=a61950a2cbd5f32876b0b55587ec7a27&optional_scopes=read_discounts,write_products

Shopify's API libraries wrap this in a helper method, so you don't build the URL yourself:

LibraryMethod
App Bridge APIshopify.scopes.request()
React Router APIscopes.request()

The App Bridge method is client-side and asynchronous, so it doesn't redirect the browser:

shopify.scopes.request(['read_discounts', 'write_products']);

The React Router method handles the request server-side. If the scopes aren't already granted, it performs a full-page redirect to the request URL:

const {scopes} = await authenticate.admin(request);
await scopes.request(['read_discounts', 'write_products']);

Anchor to Revoke granted scopes dynamicallyRevoke granted scopes dynamically

Note

You can only revoke scopes that are configured as optional_scopes and that were dynamically granted. Access scopes configured in the scopes field can't be revoked dynamically.

If your app no longer needs certain access scopes from a merchant's store, then we recommend revoking them. This helps avoid a potential data leak if the access token is ever compromised.

Revoke access scopes with a GraphQL Admin API mutation:

Example request to revoke granted optional access scopes

POST https://{shop}.myshopify.com/admin/api/{api_version}/graphql.json

mutation {
appRevokeAccessScopes(scopes: ["read_discounts","write_products"]) {
revoked {
handle
}
userErrors {
field
message
}
}
}

Response

{
"data": {
"appRevokeAccessScopes": {
"revoked": [
{
"handle": "read_discounts"
},
{
"handle": "write_products"
}
],
"userErrors": []
}
}
}

Both libraries expose a revoke helper that wraps this mutation:

LibraryMethod
App Bridge APIshopify.scopes.revoke()
React Router APIscopes.revoke()

Anchor to Troubleshoot scope problemsTroubleshoot scope problems

What a scope failure looks like depends on what's blocking the call:

SymptomWhat it meansWhat to do
A call returns ACCESS_DENIED in errors[n].extensions.codeThe token making the call doesn't carry the scope that field or mutation needs. On the GraphQL Admin API the response is an HTTP 200 with data: null, so error handling that branches on the status code won't catch it.When the requirement is a scope, the message names it, as in Access denied for shopifyqlQuery field. Required access: read_reports access scope. If you've already declared that scope, query the scopes you were actually granted: a declared scope takes effect only after you deploy the configuration and the merchant approves it. If it's declared and granted, then the gate isn't a scope you can declare, and the reference page for the field or mutation you're calling documents any other requirements.
A bulk operation returns FAILED with errorCode: ACCESS_DENIEDA missing scope, but a bulk operation doesn't report which field needed it.Run the same query as a normal, non-bulk request to get the field-level error. See Operation failures.
A call fails for one staff member but succeeds for another, or fails with an online access token and works with an offline oneYour app has the scope, but the staff member doesn't have the matching permission. Shopify applies that narrowing per request, so your app's own scopes still look correct.Compare associated_user_scope from the grant response against the scopes your app declares. It's the intersection of your app's scopes and that user's permissions, narrowed to what the staff member can do. Remember that a granted write scope carries its read counterpart, so don't read an absent read_ handle as a missing permission. The merchant adjusts staff permissions in the Shopify admin.
A protected customer field returns null and errors[n].message reads This app is not approved to access the Customer object.Not a missing scope. Your app has the scope, and Shopify is withholding the field. Unlike ACCESS_DENIED, data is still populated: approved fields return values, only the protected ones come back null, and errors[n].path points at the field.Select the customer data and fields your app uses in the Partner Dashboard. If your app is installed only on development stores, that's all you need. Every other app also has to meet the protected customer data requirements and be approved.
A scope is rejected when your app requests itThe scope needs Shopify's approval before your app can request it.The OAuth error reads missing_shopify_permission: <scope>, and the same failure on the GraphQL path reads Shopify needs to approve the following access scopes: <names>. Follow Requesting specific permissions to request access.
Deploying fails with Declared optional_scopes [read_products] cannot be implicit required scopes.A read scope is declared as optional while its write_ counterpart is required.Remove the read scope from optional_scopes. The write scope already grants it, so declaring it optional is redundant. If you want the read scope to genuinely be optional, the write scope can't stay required. See Moving a scope between scopes and optional_scopes.


Was this page helpful?