---
title: Manage access scopes
description: >-
  Configure required and optional access scopes, request and revoke optional
  scopes dynamically, and query the scopes granted to your app.
source_url:
  html: >-
    https://shopify.dev/docs/apps/build/authentication-authorization/manage-access-scopes
  md: >-
    https://shopify.dev/docs/apps/build/authentication-authorization/manage-access-scopes.md
---

# 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](https://shopify.dev/docs/api/usage/access-scopes). For how your app gets the access token that carries these scopes, see [About app authentication](https://shopify.dev/docs/apps/build/authentication-authorization).

***

## Where you configure scopes

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

| How your app is set up | Where you declare scopes |
| - | - |
| [Built with Shopify CLI](https://shopify.dev/docs/apps/build/cli-for-apps) | The `[access_scopes]` section of your app's [configuration TOML file](https://shopify.dev/docs/apps/build/cli-for-apps/app-configuration#access_scopes). Run `shopify app deploy` to deploy the scopes you've modified. |
| [Created in the Dev Dashboard](https://shopify.dev/docs/apps/build/dev-dashboard/create-apps-using-dev-dashboard#step-2-create-a-version) | Your app's version in the Dev Dashboard. Release the version to deploy the scopes you've modified. |
| Implements the [authorization code grant](https://shopify.dev/docs/apps/build/authentication-authorization/access-tokens#authorization-code-grant) itself | The `scope` parameter of the [authorization URL](https://shopify.dev/docs/apps/build/authentication-authorization/authenticate-standalone-apps#build-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.

***

## Choose your scopes

Most apps start with a small set of scopes. The most frequently used [GraphQL Admin API](https://shopify.dev/docs/api/admin-graphql) scopes are [`read_products`](https://shopify.dev/docs/api/usage/access-scopes#read_products), [`read_orders`](https://shopify.dev/docs/api/usage/access-scopes#read_orders), [`read_customers`](https://shopify.dev/docs/api/usage/access-scopes#read_customers), [`read_inventory`](https://shopify.dev/docs/api/usage/access-scopes#read_inventory), [`read_draft_orders`](https://shopify.dev/docs/api/usage/access-scopes#read_draft_orders), [`read_discounts`](https://shopify.dev/docs/api/usage/access-scopes#read_discounts), [`read_content`](https://shopify.dev/docs/api/usage/access-scopes#read_content), and [`read_themes`](https://shopify.dev/docs/api/usage/access-scopes#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](https://shopify.dev/docs/api/admin-graphql). For the full list of scopes, including [unauthenticated](https://shopify.dev/docs/api/usage/access-scopes#unauthenticated-access-scopes) and [customer](https://shopify.dev/docs/api/usage/access-scopes#customer-access-scopes) scopes, see the [access scopes reference](https://shopify.dev/docs/api/usage/access-scopes).

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`](#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`](https://shopify.dev/docs/api/usage/access-scopes#orders-permissions) 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](https://shopify.dev/docs/apps/launch/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](#modify-declared-scopes).

***

## Access scope configurations

There are two ways you can [configure your access scopes](https://shopify.dev/docs/apps/build/cli-for-apps/app-configuration#access_scopes): `scopes`, which merchants grant when they install your app, and `optional_scopes`, which your app requests later and merchants can decline.

### `scopes`

Required access scopes. Merchants must grant these when they [install your app](https://shopify.dev/docs/apps/build/authentication-authorization/cli-app-authentication), so your app is guaranteed to have them after installation.

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

```toml
# 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"
```

### `optional_scopes`

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](#request-new-access-scopes-dynamically) in the `optional_scopes` field of your app's TOML file:

```toml
# 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](#request-new-access-scopes-dynamically) after installation completes, if it needs them.

***

## Modify declared scopes

To add or remove access scopes for your app, update your app's [configuration TOML file](https://shopify.dev/docs/apps/build/cli-for-apps/app-configuration#access_scopes) 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

   ```terminal
   shopify app deploy
   ```

3. Optionally, subscribe to the [`app/scopes_update`](https://shopify.dev/docs/api/webhooks/latest?reference=toml#list-of-topics-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](https://shopify.dev/docs/apps/build/dev-dashboard/create-apps-using-dev-dashboard#step-2-create-a-version) with the scopes you want. For an app that implements the authorization code grant itself, [send the merchant through the authorization URL again](https://shopify.dev/docs/apps/build/authentication-authorization/authenticate-standalone-apps#confirm-the-granted-scopes) with the updated scope list. In every case, merchants are prompted to approve scopes you add.

### Modifying the `scopes` field

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.

### Modifying the `optional_scopes` field

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

* Your app can start [requesting the new access scopes](#request-new-access-scopes-dynamically).
* 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.

### 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](#request-new-access-scopes-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:

```text
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.

***

## Query currently granted scopes

Use a [GraphQL Admin API query](https://shopify.dev/docs/api/admin-graphql/latest/queries/currentAppInstallation) 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

```graphql
query {
  currentAppInstallation {
    accessScopes {
      description
      handle
    }
  }
}
```

## Response

```json
{
  "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:

| Library | Method |
| - | - |
| App Bridge API | [`shopify.scopes.query()`](https://shopify.dev/docs/api/app-home/apis/authentication-and-data/scopes-api#scopes-propertydetail-query) |
| React Router API | [`scopes.query()`](https://shopify.dev/docs/api/shopify-app-react-router/latest/apis/scopes#scopes-propertydetail-query) |

***

## Request 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](https://shopify.dev/assets/assets/images/apps/auth/optional-scopes-grant-modal-DKVaj88Z.png)

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

```text
https://admin.shopify.com/store/{STORE_NAME}/oauth/install?client_id={CLIENT_ID}&optional_scopes={REQUESTED_SCOPES}
```

| Query parameter | Description |
| - | - |
| `STORE_NAME` | The name of the merchant's store. |
| `CLIENT_ID` | The app's client ID. |
| `REQUESTED_SCOPES` | A comma-separated list of access scopes to request. This must be a subset of the declared `optional_scopes` in your TOML file. |

For example:

```text
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:

| Library | Method |
| - | - |
| App Bridge API | [`shopify.scopes.request()`](https://shopify.dev/docs/api/app-home/apis/authentication-and-data/scopes-api#scopes-propertydetail-request) |
| React Router API | [`scopes.request()`](https://shopify.dev/docs/api/shopify-app-react-router/latest/apis/scopes#scopes-propertydetail-request) |

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

```javascript
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:

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

***

## Revoke granted scopes dynamically

**Note:**

You can only revoke scopes that are configured as `optional_scopes` and that were [dynamically granted](#request-new-access-scopes-dynamically). 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](https://shopify.dev/docs/api/admin-graphql/latest/mutations/appRevokeAccessScopes):

## Example request to revoke granted optional access scopes

## POST https://{shop}.myshopify.com/admin/api/{api\_version}/graphql.json

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

## Response

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

Both libraries expose a revoke helper that wraps this mutation:

| Library | Method |
| - | - |
| App Bridge API | [`shopify.scopes.revoke()`](https://shopify.dev/docs/api/app-home/apis/authentication-and-data/scopes-api#scopes-propertydetail-revoke) |
| React Router API | [`scopes.revoke()`](https://shopify.dev/docs/api/shopify-app-react-router/latest/apis/scopes#scopes-propertydetail-revoke) |

***

## Troubleshoot scope problems

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

| Symptom | What it means | What to do |
| - | - | - |
| A call returns `ACCESS_DENIED` in `errors[n].extensions.code` | The 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](#query-currently-granted-scopes): 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_DENIED` | A 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](https://shopify.dev/docs/apps/build/apis/graphql-admin/bulk-operations/queries#operation-failures). |
| A call fails for one staff member but succeeds for another, or fails with an [online access token](https://shopify.dev/docs/apps/build/authentication-authorization/access-tokens#online-access-tokens) and works with an offline one | Your 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](https://shopify.dev/docs/apps/launch/protected-customer-data#requirements) and be approved. |
| A scope is rejected when your app requests it | The 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](https://shopify.dev/docs/api/usage/access-scopes#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`](#moving-a-scope-between-scopes-and-optional_scopes). |

***

## Next steps

* Look up the full list of scopes and what each one grants in the [access scopes reference](https://shopify.dev/docs/api/usage/access-scopes).
* Learn how to use the `scopes` API in the [App Bridge library](https://shopify.dev/docs/api/app-home/apis/authentication-and-data/scopes-api) or the [React Router library](https://shopify.dev/docs/api/shopify-app-react-router/latest/apis/scopes).
* Review how to declare access scopes in your [app's configuration file](https://shopify.dev/docs/apps/build/cli-for-apps/app-configuration#access_scopes).
* Understand how [access tokens](https://shopify.dev/docs/apps/build/authentication-authorization/access-tokens) carry the scopes a merchant granted.

***
