# Scopes

_Category: Authentication_

A scope is a permission that a credential holds. REST API endpoints each require a scope, and API keys and OAuth access tokens are checked against the same list: a request that needs `content.write` fails the same way for a Read only key as for a token without that scope.

```
{
  "status": "error",
  "code": "insufficient_scope",
  "message": "This action requires the content.write scope."
}
```

## The scopes

The third column is what the consent screen shows a person when an OAuth app asks for the scope.

| Scope | Allows | Consent screen text |
| --- | --- | --- |
| `content.read` | Reading the help center: sites, articles, categories, images, comments, staged changes and versions, change sets, FAQ sections, the widget's install details, interface translations and exports. | Read your help-center categories and articles. |
| `content.write` | Every change to content: creating, updating and deleting articles and categories, uploading and deleting images, replying to, moderating and deleting comments, staging and publishing changes, restoring versions, change sets, FAQ sections, interface translations and relaying reader feedback. | Create and update your help-center articles. |
| `analytics.read` | The analytics reports, and reader feedback: the votes and messages readers leave on articles. | Read your help-center analytics reports. |
| `webhooks.manage` | Listing, creating and deleting webhooks. | Register webhooks that notify it when your content changes. |
| `notes.read` | Reading the private notes your team leaves on articles. | Read the private notes your team leaves on drafts. Readers never see these. |
| `notes.write` | Starting notes on passages, and replying to and resolving notes. | Reply to and resolve the private notes your team leaves on drafts. |
| `design.read` | Reading the help center's design: the draft, the customization guide, the themes and the published versions. | See your help center's design: its theme, page layouts, components, custom CSS and scripts. |
| `design.write` | Every change to the design: editing the draft, applying themes, discarding the draft, restoring versions into it, and publishing it. That includes the custom CSS and scripts every visitor's browser runs once the design is published. | `Change your help center's design — theme, layouts, components, custom CSS and scripts — and publish it.` |
| `domain.manage` | Reading, setting and removing the help center's custom domain. | Connect your help center to your own domain, check its DNS and certificate, and remove the domain. |
| `embed.manage` | Changing which websites may embed the help center and run its widget: its embedding origins. | Choose which websites may embed your help center and run its widget. This changes your help center's security headers. |

`content.write` covers every change to content, including deleting articles and categories, deleting images and moderating comments. If your app asks for it, tell your users what the app will do with it.

`design.read` and `design.write` aren't part of `content.read` and `content.write`: a credential needs them to read or change the design, whatever content scopes it holds. See [Design API](https://developers.helpcenter.io/content/design-api).

## Endpoints by scope

Each endpoint requires exactly one scope, with three kinds of exception: two endpoints any valid credential may call, the single-use image upload URL, which is its own credential, and the requests an AI agent makes before it has a key. Creating a help center and filing a migration request take no credential, and a migration request's status takes the status token that filing it returned (see [Migrate a knowledge base as an AI agent](https://developers.helpcenter.io/content/migrate-a-knowledge-base-as-an-ai-agent)).

| Scope | Endpoints |
| --- | --- |
| None (any valid credential) | `GET /v1`, `GET /v1/account` |
| None (no credential) | `POST /v1/agent/help-centers`, `POST /v1/agent/migration-requests` |
| The request's status token | `GET /v1/agent/migration-requests/{id}` |
| `content.read` | Sites: `GET /v1/sites`<br><br><br>Articles: `GET /v1/articles`, `GET /v1/articles/{articleId}`, `GET /v1/articles/{articleId}/staged`, `GET /v1/articles/{articleId}/versions`<br><br><br>Categories: `GET /v1/categories`<br><br><br>Images: `GET /v1/images`, `GET /v1/images/{imageId}`<br><br><br>Comments: `GET /v1/comments`, `GET /v1/articles/{articleId}/comments`<br><br><br>Change sets: `GET /v1/change-sets`, `GET /v1/change-sets/{changeSetId}`, `GET /v1/change-sets/{changeSetId}/status`<br><br><br>Widget and FAQ sections: `GET /v1/widget`, `GET /v1/faq-sections`, `GET /v1/faq-sections/{sectionId}`<br><br><br>Redirects (API keys only): `GET /v1/redirects`<br><br><br>AI agents (the key of the agent that created the help center): `GET /v1/agent/help-centers/{id}`<br><br><br>Other: `GET /v1/translations`, `GET /v1/export` |
| `content.write` | Articles: `POST /v1/articles`, `POST /v1/articles/bulk`, `PATCH /v1/articles/{articleId}`, `DELETE /v1/articles/{articleId}`, `POST /v1/articles/{articleId}/feedback`<br><br><br>Staged changes and versions: `PATCH /v1/articles/{articleId}/staged`, `DELETE /v1/articles/{articleId}/staged`, `POST /v1/articles/{articleId}/staged/publish`, `POST /v1/articles/{articleId}/versions/{versionId}/restore`<br><br><br>Categories: `POST /v1/categories`, `PATCH /v1/categories/{categoryId}`, `DELETE /v1/categories/{categoryId}`, `PUT /v1/categories/order`, `PUT /v1/categories/{categoryId}/articles/order`<br><br><br>Images: `POST /v1/images`, `POST /v1/images/bulk`, `POST /v1/images/uploads`, `DELETE /v1/images/{imageId}`<br><br><br>Comments: `POST /v1/articles/{articleId}/comments`, `PATCH /v1/comments/{commentId}`, `DELETE /v1/comments/{commentId}`<br><br><br>Interface translations: `POST /v1/translations`, `POST /v1/translations/{translationKey}`<br><br><br>Change sets: `POST /v1/change-sets`, `PATCH /v1/change-sets/{changeSetId}`, `DELETE /v1/change-sets/{changeSetId}`, `POST /v1/change-sets/{changeSetId}/items`, `DELETE /v1/change-sets/{changeSetId}/items/{itemId}`, `POST /v1/change-sets/{changeSetId}/publish`, `DELETE /v1/change-sets/{changeSetId}/schedule`<br><br><br>FAQ sections: `POST /v1/faq-sections`, `PATCH /v1/faq-sections/{sectionId}`, `DELETE /v1/faq-sections/{sectionId}`, `POST /v1/faq-sections/{sectionId}/articles`, `PUT /v1/faq-sections/{sectionId}/articles/order`, `DELETE /v1/faq-sections/{sectionId}/articles/{articleId}`<br><br><br>Imports, reading included (an OAuth token also needs an Owner or Admin): `GET /v1/imports`, `POST /v1/imports`, `GET /v1/imports/{importId}`, `PUT /v1/imports/{importId}`, `POST /v1/imports/{importId}/run`, `GET /v1/imports/{importId}/redirect-map`, `DELETE /v1/imports/{importId}`<br><br><br>Redirects (API keys only): `POST /v1/redirects`, `POST /v1/redirects/bulk`, `DELETE /v1/redirects/{redirectId}`<br><br><br>AI agents (the key of the agent that created the help center): `DELETE /v1/agent/help-centers/{id}`, `POST /v1/agent/help-centers/{id}/migration-request` |
| `analytics.read` | `GET /v1/analytics/summary`, `GET /v1/analytics/content`, `GET /v1/analytics/searches`, `GET /v1/feedback` |
| `webhooks.manage` | `GET /v1/webhooks`, `POST /v1/webhooks`, `DELETE /v1/webhooks/{webhookId}` |
| `notes.read` | `GET /v1/articles/{articleId}/notes` |
| `notes.write` | `POST /v1/articles/{articleId}/notes`, `POST /v1/articles/{articleId}/notes/{noteId}/replies`, `POST /v1/articles/{articleId}/notes/{noteId}/resolve`, and the `notes` and `annotate_changes` fields of `PATCH /v1/articles/{articleId}` and `PATCH /v1/articles/{articleId}/staged` |
| `design.read` | `GET /v1/template`, `GET /v1/template/draft`, `GET /v1/template/schema`, `GET /v1/template/themes`, `GET /v1/template/themes/{themeKey}`, `GET /v1/template/versions` |
| `design.write` | `PATCH /v1/template/draft`, `POST /v1/template/draft/discard`, `POST /v1/template/themes/{themeKey}/apply`, `POST /v1/template/publish`, `POST /v1/template/versions/{versionId}/restore` |
| `domain.manage` | `GET /v1/custom-domain`, `PUT /v1/custom-domain`, `DELETE /v1/custom-domain` |
| `embed.manage` | `PATCH /v1/widget/embedding-origins` |
| None (the URL is the credential) | `POST /v1/images/uploads/{intent}`, the upload URL that `POST /v1/images/uploads` returns |

Redirects accept only API keys: an OAuth token gets `403` with the code `api_key_required`. See [Redirects](https://developers.helpcenter.io/content/redirects-api). [Imports](https://developers.helpcenter.io/content/imports-api) and the [custom domain](https://developers.helpcenter.io/content/custom-domain-api) accept OAuth tokens whose person is an Owner or Admin.

## How API keys map to scopes

An API key doesn't list scopes itself. Its scope in the dashboard, and its **Team notes**, **Design** and **Custom domain** settings, give it these. A fourth setting, **Redirects**, adds no scope: it lets the key's redirects lead to other sites (see [Redirects](https://developers.helpcenter.io/content/redirects-api)).

| API key | Scopes it holds |
| --- | --- |
| **Read only** | `content.read`, `analytics.read` |
| **Read & write** | `content.read`, `content.write`, `analytics.read`, `webhooks.manage`, `embed.manage` |
| **Read only** with **Team notes** | The read-only scopes, plus `notes.read` |
| **Read & write** with **Team notes** | The read and write scopes, plus `notes.read` and `notes.write` |
| **Read only** with **Design** | The read-only scopes, plus `design.read` |
| **Read & write** with **Design** | The read and write scopes, plus `design.read` and `design.write` |
| **Read & write** with **Custom domain** | The read and write scopes, plus `domain.manage` |
| An AI agent's key (named **AI agent**, or **AI agent (setup)** once its user keeps it) | The read and write scopes, plus `design.read`, `design.write` and `domain.manage` |

An AI agent's key is the one `POST /v1/agent/help-centers` returns. It expires, and until someone claims its help center it reaches only that help center's content and design. A key its user lets the agent keep after the claim reaches the help center's content, design, redirects and custom domain; every other endpoint answers `403` with `agent_not_allowed`. See [Migrate a knowledge base as an AI agent](https://developers.helpcenter.io/content/migrate-a-knowledge-base-as-an-ai-agent).

A Read only key never holds `notes.write`, `design.write`, `domain.manage` or `embed.manage`. A key holds the design scopes only when **Allow access to the help center design** is ticked for it, and `domain.manage` only when **Allow managing the custom domain** is ticked for a Read & write key. An Owner or Admin sets these when they create the key, or later with **Edit access** on it in Settings; the change applies from the key's next request. To see what a credential can do, call `GET /v1/account`: `kb.capabilities.read` and `kb.capabilities.write` tell you whether it holds `content.read` and `content.write`. See [API keys](https://developers.helpcenter.io/content/api-keys).

## How OAuth tokens get scopes

An OAuth app asks for scopes in the `scope` parameter of the authorization request, separated by spaces:

```
scope=openid email content.read content.write
```

- **All or nothing.** The consent screen lists every requested scope with its description. The person approves the whole list or denies it; they can't pick some scopes and leave others.
- **A token holds what its request asked for.** The granted scopes are in the access token's `scopes` claim, an array. The token response has no `scope` field.
- **Unknown scopes fail.** A scope that isn't on this page ends the authorization with the error `invalid_scope`.
- **No scope, no content.** An authorization request without `scope` gives a token with no scopes. It can call `GET /v1` and `GET /v1/account`; everything else answers `403` with `insufficient_scope`.
- **Asking for more later.** Send the person through authorization again with the longer list. The consent screen appears whenever you ask for a scope they haven't approved for your app; when every scope was approved before, the browser returns to your app without it. What a person approves adds up: a later approval doesn't take back scopes they approved earlier.
- **Refreshing can narrow, not widen.** A refresh request can pass `scope` with fewer scopes than the original token. Asking for one the original didn't have fails with `invalid_scope`.

The full flow is in [OAuth 2.1 for apps and AI clients](https://developers.helpcenter.io/content/oauth).

## OpenID Connect scopes

These identify the person who approved your app. None of them opens a REST API endpoint.

| Scope | What you get | Consent screen text |
| --- | --- | --- |
| `openid` | An `id_token` in the token response. | Authenticate you and confirm your identity. |
| `profile` | The claims `name`, `given_name`, `family_name` and `picture` in the id_token and from userinfo. | Access your basic profile information (name, picture). |
| `email` | The claims `email` and `email_verified`. | Access your email address. |
| `offline_access` | Nothing extra. Every app gets a refresh token whether or not it asks for this scope; it's accepted for clients that always request it. | Stay connected, so you are not asked to sign in again each time its short-lived access expires. |

## Team notes need scope and access

Holding `notes.read` or `notes.write` isn't enough on its own. On every notes request, the person behind the credential (the creator of the API key, or the person who approved the OAuth app) must be an Owner, Admin, Editor or Translator of the help center. If they aren't, the request fails with `403`, the code `notes_forbidden` and this message: "Editorial notes are visible only to team members who can access team comments in the dashboard. The account this credential authenticates as no longer has that access on this help center."

## The design needs scope and access

Holding `design.read` or `design.write` isn't enough on its own either. On every design request, the person behind the credential must be able to edit the help center's design in the dashboard: an Owner, Admin or Editor. If they can't, the request fails with `403`, the code `design_forbidden` and this message: "Only team members who can edit this help center's design in the dashboard (editors, admins and owners) can read or change it through the API. The account this credential authenticates as no longer has that access on this help center."

## The custom domain and the embedding list need scope and role

Holding `domain.manage` or `embed.manage` isn't enough either. The person behind the credential must be an Owner or Admin of the help center, as in the dashboard, where only they can change its domain and which websites may embed it. An API key's creator always is: their keys stop working when they aren't. An OAuth token whose person is, for example, an Editor gets `403` with the code `role_forbidden` and their `role`.

## When a scope is missing

A credential without the scope an endpoint needs gets `403 Forbidden` with the body at the top of this page. The `message` always names the missing scope, for API keys and OAuth tokens alike. Retrying won't help:

- With an API key, an Owner or Admin can add a setting the key lacks: **Edit access** on the key in Settings, under **API keys**, then **Team notes**, **Design** or **Custom domain**. A key's **Read only** or **Read & write** scope can't change, so a Read only key that needs a write scope is replaced by a new Read & write key. For `domain.manage`, a key's message goes on to name the setting: `This action requires the domain.manage scope. An API key holds it only when it is a Read & write key and one of the help center's Owners or Admins has ticked "Allow managing the custom domain" for it in Settings > API keys.`
- With an OAuth token, send the person through authorization again with the scope added.

Two other errors look similar. `401` means the credential itself isn't valid. `403` with `site_forbidden` means the token doesn't reach the help center you named (see [Choose which help center a request acts on](https://developers.helpcenter.io/content/choosing-a-help-center)). Every code is listed in [Errors](https://developers.helpcenter.io/content/errors).

## AI assistants and their first connection

When an AI assistant connects to the HelpCenter.io MCP server without credentials, the server's sign-in challenge asks for every scope one of its tools uses, and its metadata lists those and `webhooks.manage`, which no tool uses. An assistant that asks for fewer scopes than the challenge gets a token without the rest. The tools that need them are then refused with an error that names the missing permission, until the person connects the assistant again and approves it. Which scopes an assistant asks for is up to the assistant. See [Connect an AI assistant to your help center](https://developers.helpcenter.io/content/connect-an-ai-assistant-to-your-help-center).

## Related

- [OAuth 2.1 for apps and AI clients](https://developers.helpcenter.io/content/oauth)
- [API keys](https://developers.helpcenter.io/content/api-keys)
- [Errors](https://developers.helpcenter.io/content/errors)
- [Choose which help center a request acts on](https://developers.helpcenter.io/content/choosing-a-help-center)
