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.
The consent model
Scopes flow through four stages, from declaration to enforcement.
- 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. - Consent — When a merchant installs your plugin, they see the requested scopes and consent to them. Installation is how the grant happens.
- Grant — The consented set becomes the
scopesclaim inside every token issued for that install (both session and access tokens carry it). - Enforce — Each API endpoint checks the token's
scopesclaim. If the required scope is present, the call proceeds; if not, it is rejected.
{
"oauth": {
"scopes": ["product:list", "product:read", "order:read", "webhook:create"]
}
}
The token your plugin uses carries exactly this granted set:
{
"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 one of exactly five verbs:
| Action | Meaning |
|---|---|
list | Enumerate many records |
read | Read a single record |
create | Create a record |
update | Modify a record |
delete | Remove a record |
So product:list, customer:read, and order:create are all valid.
Common mistakes
The two most frequent errors are pluralising the resource and inventing an action verb.
| Wrong | Right | Why |
|---|---|---|
products:read | product:read | Resource is singular, not plural |
orders:list | order:list | Resource is singular, not plural |
order:write | order:create / order:update | write is not an action — use create or update |
product:readonly | product:read | readonly 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.
| Resource | Scopes |
|---|---|
product | product:list, product:read, product:create, product:update |
order | order:list, order:read, order:create |
customer | customer:list, customer:read |
taxonomy | taxonomy:list, taxonomy:read |
webhook | webhook: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(andproduct:listif you enumerate them) — notproduct: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:
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/JWT decoder works:
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 tells you which stage failed.
| Status | Meaning | What to do |
|---|---|---|
401 | The token is missing, malformed, or expired | Re-authenticate. In the iframe, ensure await orbit.ready() has resolved; on the backend, refresh the access token. See authentication. |
403 | The token is valid but lacks the required scope | Add the scope to your manifest's oauth.scopes and have the merchant re-install/re-consent. |
A 401 is an authentication problem; a 403 is a scope problem. Distinguishing them tells you whether to refresh a token or request a scope.
Reference
- Scope Catalog — the complete list of available scopes.
- API Reference — the exact scope required by each endpoint.
- Authentication — how tokens are issued, refreshed, and verified.
- Webhooks — topic subscriptions and their required scopes.