Skip to content

OAuth scopes

Scopes are the permissions a plugin requests and a merchant grants. They define exactly what your plugin can read and write on a store.

What a scope is

A scope is a single permission string of the form <resource>:<action>. Each scope authorises one kind of operation on one kind of resource: for example, product:read lets your plugin read products, and order:create lets it create orders. Your plugin holds a set of scopes, and every API endpoint requires specific scopes to call it.

Scopes flow through four stages, from declaration to enforcement.

  1. Request. You declare the scopes your plugin needs in its manifest under oauth.scopes. This is part of the plugin definition you create in the partner dashboard.
  2. Consent. When a merchant installs your plugin, they see the requested scopes and consent to them. Installation is how the grant happens.
  3. Grant. The consented set becomes the scopes claim inside every token issued for that install (both session and access tokens carry it).
  4. Enforce. Each API endpoint checks the token's scopes claim. If the required scope is present, the call proceeds; if not, it is rejected.
JSON
{
  "oauth": {
    "scopes": ["product:list", "product:read", "order:read", "webhook:create"]
  }
}

The token your plugin uses carries exactly this granted set:

JSON
{
  "sub": "<StorePlugin id>",
  "storeId": "<store uuid>",
  "pluginId": "your-plugin-id",
  "scopes": ["product:list", "product:read", "order:read", "webhook:create"],
  "type": "plugin_access"
}

Naming convention

Every scope follows one rule:

<resource>:<action>
  • resource is singular and kebab-case: product, order, customer, taxonomy, tracking-script, webhook. Never plural.
  • action is almost always one of five CRUD verbs:
ActionMeaning
listEnumerate many records
readRead a single record
createCreate a record
updateModify a record
deleteRemove a record

So product:list, customer:read, and order:create are all valid.

A small number of scopes use a purpose verb instead, where a CRUD action would be misleading about the side effects: order:fulfill (fulfilling can notify the customer) and email:send. These are listed in the Scope Catalog like every other scope. Don't coin new verbs by analogy: if a scope isn't in the Scope Catalog, it won't be granted.

Common mistakes

The two most frequent errors are pluralising the resource and inventing an action verb.

WrongRightWhy
products:readproduct:readResource is singular, not plural
orders:listorder:listResource is singular, not plural
order:writeorder:create / order:updatewrite is not an action: use create or update
product:readonlyproduct:readreadonly is not an action verb

If a scope string does not parse to a known <resource>:<action> pair, it will not grant any access.

Common scopes by resource

The following are frequently used scopes. The complete, authoritative list is the Scope Catalog.

ResourceScopes
productproduct:list, product:read, product:create, product:update
orderorder:list, order:read, order:create, order:fulfill
customercustomer:list, customer:read
taxonomytaxonomy:list, taxonomy:read
webhookwebhook:create, webhook:list, webhook:read, webhook:delete

Webhook subscriptions require both a webhook scope and the topic's own scope. To subscribe to product.* topics you need webhook:create plus product:read. See the webhooks guide for the full topic-to-scope mapping.

Request the minimum

Request only the scopes your plugin actually uses. A smaller scope set is easier for merchants to approve and reduces what is at risk if a token leaks.

  • If you only read products, request product:read (and product:list if you enumerate them), not product:update.
  • Don't request write scopes (create / update / delete) when your plugin only reads.
  • Add scopes as your plugin grows rather than requesting broad access up front. Note that expanding the requested set means merchants must re-consent on install.

Check granted scopes at runtime

A merchant may not grant every scope you requested, so check what you actually hold before calling a guarded endpoint. The granted set lives in your token's scopes claim.

On your backend, read it from the verified token:

TypeScript
import { OrbitClient } from '@orbitcommerce/sdk'

const { scopes } = await OrbitClient.verifyToken(token)

if (scopes.includes('order:read')) {
  // safe to read orders
}

In the iframe, the claim travels inside the session JWT returned by orbit.getToken(). Decode its payload to inspect scopes. It is a standard JWT, so any base64url or JWT decoder works:

TypeScript
const orbit = new OrbitClient()
await orbit.ready()

const [, payload] = orbit.getToken().split('.')
const { scopes } = JSON.parse(atob(payload)) as { scopes: string[] }

if (scopes.includes('order:read')) {
  const orders = await orbit.query(/* ... */)
} else {
  orbit.toast({ message: 'Order access not granted', type: 'warning' })
}

Use these checks to hide or disable features the merchant has not authorised, rather than letting a call fail at the API.

Enforcement errors

When a guarded endpoint rejects a call, the status code, and for scope failures the error body, tells you what went wrong.

StatusMeaningWhat to do
401The token is missing, malformed or expired, or valid but lacking the required scopeIf the token is missing/expired, re-authenticate (in the iframe, ensure await orbit.ready() has resolved; on the backend, refresh the access token, as authentication describes). If the body reads Missing required scopes: …, add the scope to your manifest's oauth.scopes and have the merchant re-install/re-consent.
403The call is refused for a reason other than the endpoint's scopes: subscribing to a webhook topic without the topic's own scope; a write to a B2B route when the store's plan lacks the B2B feature (code: "FEATURE_FROZEN"); a route whose feature the plan does not include (code: "FEATURE_NOT_ENTITLED"); or a rule the request breaks, such as approving an order the company or buyer can no longer take (errorCode: "B2B_CREDIT_LIMIT_EXCEEDED", "B2B_PAY_ON_ACCOUNT_UNAVAILABLE" or "B2B_PAYMENT_TERMS_UNAVAILABLE")For a topic: add the topic's scope (for example order:read for order.*) to your manifest and re-consent. Otherwise read the body's code or errorCode: re-consenting will not help.

Both a missing or expired token and a missing endpoint scope surface as 401: read the error body to tell them apart (a scope failure names the missing scope). A 403 is never an endpoint-scope problem. An id that belongs to another store answers 404, exactly like one that never existed: your token is bound to one store.

Reference