# 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.

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](/guides/publishing).
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:

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

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](/scope-catalog) like every other scope — don't coin new verbs by analogy; if it isn't in the catalog, it won't be granted.

### 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](/scope-catalog).

| Resource | Scopes |
|----------|--------|
| `product` | `product:list`, `product:read`, `product:create`, `product:update` |
| `order` | `order:list`, `order:read`, `order:create`, `order:fulfill` |
| `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](/guides/webhooks) 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:

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

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

| Status | Meaning | What to do |
|--------|---------|------------|
| `401` | The token is missing, malformed, or expired — **or valid but lacking the required scope** | If the token is missing/expired, re-authenticate (in the iframe, ensure `await orbit.ready()` has resolved; on the backend, refresh the access token — see [authentication](/guides/authentication)). If the body reads `Missing required scopes: …`, add the scope to your manifest's `oauth.scopes` and have the merchant re-install/re-consent. |
| `403` | Subscribing to a webhook topic without the topic's own scope — or the id you referenced belongs to a different store | For topics: add the topic's scope (e.g. `order:read` for `order.*`) to your manifest and re-consent. For cross-store ids: your token is bound to one store; only reference ids that belong to it. |

Both a missing/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: it means a webhook topic-scope failure or a cross-store id.

## Reference

- [Scope Catalog](/scope-catalog) — the complete list of available scopes.
- [API Reference](/api-reference) — the exact scope required by each endpoint.
- [Authentication](/guides/authentication) — how tokens are issued, refreshed, and verified.
- [Webhooks](/guides/webhooks) — topic subscriptions and their required scopes.
