# Errors _Category: Getting started_ When a request fails, the HTTP status says what kind of problem it is. Most error bodies add a message written for people, and many add a machine-readable `code`. This page lists the error shapes and codes the API returns, what each one means and what to do about it, and ends with the problems developers run into most. ## How to handle an error 1. **Check the status code first.** A refusal from the web servers in front of the API isn't JSON, so look at the status before you parse the body. 2. **Branch on `code`** when the body has one. Change sets use `reason` instead. 3. **Show `message` to people, but don't parse it.** Messages explain the problem and can change. 4. **Expect JSON from the API.** Every error the API answers is JSON, whatever you send in `Accept`, and it never answers an error with a redirect. Only the web servers in front of it answer otherwise: a request body over 32 MB, or more requests than they allow from one address (see [Rate limits](https://developers.helpcenter.io/content/rate-limits)). For example, creating an article with a **Read only** key: ``` curl https://api.helpcenter.io/v1/articles \ -H "apikey: $HELPCENTER_API_KEY" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{ "title": { "en": "Shipping times" } }' ``` The API answers `403 Forbidden`: ``` { "status": "error", "code": "insufficient_scope", "message": "This action requires the content.write scope." } ``` ## Error shapes | Body | Status | Where you meet it | | --- | --- | --- | | `{"status": "unauthorized", "docs_url": …}` | 401 | The request has no key or token, or one the API doesn't accept. `docs_url` links the quickstart. Two more links, `create_url` and `agent_docs_url`, point an AI agent that has no key at creating a help center: see [Migrate a knowledge base as an AI agent](https://developers.helpcenter.io/content/migrate-a-knowledge-base-as-an-ai-agent). | | `{"status": "error", "message": …}` | 400, 401, 403, 404, 409, 422, 500 | Most errors from an endpoint, such as `Article not found.`, and an image that couldn't be stored (500). | | `{"status": "error", "code": …, "message": …}` | 400, 402, 403, 404, 409, 410, 412, 422, 429, 503 | Errors with a machine-readable code, sometimes with more fields. See Error codes below. | | `{"status": "error", "reason": …, "message": …}` | 402, 409 | Change sets. | | `{"status": "validation_error", "message": …, "errors": {…}}` | 422 | Validation on most endpoints, including `GET /v1/categories`, `GET /v1/images` and `PATCH /v1/articles/{id}`. | | `{"status": "validation_error", "errors": {…}}` | 400 | Validation on `GET /v1/articles`, except a `lang` the help center doesn't have on a search, which answers `422` with `"status": "validation_error"`, a `message` and `errors`. | | `{"": […]}` | 400 | Validation on `POST /v1/articles`. | | `{"message": …}` | 405, 429, 500 | General errors: a method the path doesn't take, a rate limit, a failure on HelpCenter.io's side. | | `{"status": "error", "message": "Not found."}` | 404 | An unknown path. | | An HTML page | 413, 429 | A refusal from the web servers in front of the API: a request body over 32 MB, or more requests than they allow from one address. | ## Validation errors Validation errors list the problems by field: every key of `errors` is a field you sent, and every value a list of messages. The envelope around them depends on the endpoint. Most endpoints answer `422 Unprocessable Entity`, as here for a category name sent as a string: ``` { "status": "validation_error", "message": "The request could not be accepted.", "errors": { "name": [ "The name must be a {locale: text} map — for example {\"en\": \"Billing\"}." ] } } ``` `GET /v1/articles` answers `400 Bad Request` without a `message`, here for `limit=500`: ``` { "status": "validation_error", "errors": { "limit": ["The limit field must not be greater than 100."] } } ``` `POST /v1/articles` answers `400` with the fields at the top level, here for an article without a title: ``` {"title": ["The title field is required."]} ``` `GET /v1/categories`, `GET /v1/images` and `PATCH /v1/articles/{id}` answer `422` with the same envelope as the other endpoints. Here `GET /v1/categories?limit=500`: ``` { "status": "validation_error", "message": "The request could not be accepted.", "errors": { "limit": ["The limit field must not be greater than 100."] } } ``` Bulk requests validate every item on its own, inside a `207` response. Read `results`: the top-level `status` is `success` even when every item failed. ``` { "status": "success", "results": [ { "index": 0, "status": "error", "external_id": null, "error": { "code": "validation_error", "message": "The article could not be accepted.", "errors": {"title": ["The title field must be an array."]} } }, { "index": 1, "status": "error", "error": { "code": "invalid_item", "message": "Each item must be an article object." } }, { "index": 2, "status": "error", "external_id": null, "error": { "code": "invalid_argument", "message": "Unsupported language key \"fr\" provided." } } ], "meta": {"total": 3, "created": 0, "updated": 0, "failed": 3} } ``` ## Status codes | Status | What it means | What to do | | --- | --- | --- | | `400 Bad Request` | Validation on `GET` and `POST /v1/articles`, a language code your help center doesn't have (`Unsupported language key "fr" provided.`), or `site_required`. | Fix the request. | | `401 Unauthorized` | No credential, or one the API doesn't accept: a missing, mistyped, deleted or expired key, a key whose creator is no longer an Owner or Admin of the help center or whose HelpCenter.io account was deleted, or an OAuth token that expired or was revoked. | Check the `apikey` header, or refresh the token. | | `402 Payment Required` | The help center doesn't have this feature: `plan_upgrade_required` for requests through the MCP server, `CHANGE_SETS_UNAVAILABLE` for change sets. | Change the plan, or for change sets on Catalyst, request early-preview access. Retrying doesn't help. | | `403 Forbidden` | The credential is valid but may not do this: `insufficient_scope`, `site_forbidden`, `notes_forbidden`, `design_forbidden`, `role_forbidden`, `STAGING_UNAVAILABLE`, `upload_url_invalid`, `upload_url_revoked`, `api_key_required` or `custom_domain_not_in_plan`, and for an AI agent's key `agent_not_allowed`, `capability_locked` or `agent_scripts_forbidden`. Also asking for commenters' email addresses with a Read only key. | See the code in the table below. | | `404 Not Found` | Nothing with that id exists on the help center your credential acts on. That includes ids from other help centers and items in the Trash. A key whose help center was deleted gets `The site this API key belongs to is no longer available.` The design endpoints answer with a code, listed below. | Check the id, and which help center your key belongs to: `GET /v1/sites` tells you. | | `405 Method Not Allowed` | The path exists, but not with this method. | Check the method in the reference. | | `409 Conflict` | The request clashes with the current state: `category_not_empty`, `STALE_ETAG`, `BASE_CHANGED`, `template_migration_required`, `nothing_to_publish`, a change set `reason`, an `external_id` that another article holds or that another request is importing, an image that articles still use, an import that is running or was cancelled (`import_running`, `import_cancelled`, `not_importable`, `main_language_changed`), a domain in use or being changed (`domain_taken`, `custom_domain_busy`), or for an AI agent's key `migration_in_progress`, `already_claimed` or `source_unavailable`. | Read the body: it says what clashed, and often what to send instead. | | `410 Gone` | An upload URL expired or was already used, or `expired`: an AI agent's key whose help center expired before anyone claimed it. | Create another upload URL with `POST /v1/images/uploads`. For `expired`, start again with `POST /v1/agent/help-centers`. | | `412 Precondition Failed` | `stale_draft`: the design draft changed since the `etag` you sent, so nothing changed. | Read the draft again, make your change on top of it, and retry with the new `etag`. | | `413 Payload Too Large` | The request body is over 32 MB. The web servers in front of the API refuse it, with a page that isn't JSON. | Send fewer or smaller files per request. | | `422 Unprocessable Entity` | Validation failed, or the request is valid but can't be carried out, such as `NOT_PUBLISHED`, `template_invalid`, `limit_reached`, an import source that can't be read, or a redirect that can't be saved. | Fix the fields in `errors`, or follow the message. | | `429 Too Many Requests` | You hit a rate limit, or a limit with a code: `limit_reached` (creating help centers or filing migration requests without an account, reading a migration request) or `custom_domain_limit_reached`. | Wait for `Retry-After` seconds. See [Rate limits](https://developers.helpcenter.io/content/rate-limits). | | `500 Internal Server Error` | Something failed on HelpCenter.io's side. The body is `{"message": "Server Error"}`. | Retry later. If it keeps happening, [contact support](https://self.helpcenter.io/content/contact-support) with the method, path, time and body of the request, never your key. | | `503 Service Unavailable` | `analytics_unavailable`: the analytics are temporarily unavailable. `capacity_reached` or `busy`: no help center can be created without an account right now. | Retry after `Retry-After` seconds (300 for the analytics, 5 for `busy`). For `capacity_reached`, try later or file a migration request. | ### 401, 403 or 404? `401` means the API doesn't know who you are. `403` means it knows, but your credential may not do this. `404` means there is nothing with that id on the help center your credential acts on: an id from another help center always answers `404`, never `403`, so an error never confirms that an id exists somewhere else. ``` {"status": "error", "message": "Article not found."} ``` An unknown id answers `404` with a JSON body on every route, such as `Article not found.` or `Category not found.`, and an unknown path answers `{"status": "error", "message": "Not found."}`. ## Error codes These codes appear in the `code` field of REST API errors. The last five, marked "reason", appear in the `reason` field of change set errors. | Code | Status | What it means | What to do | | --- | --- | --- | --- | | `insufficient_scope` | 403 | The credential lacks the scope this endpoint needs. The message names it: `This action requires the content.write scope.` For the custom domain, an API key's message also names the setting to tick. | Use a **Read & write** key (for team notes, one with team notes access; for the design, one with design access; for the custom domain, one that an Owner or Admin allowed to manage the custom domain, with **Allow managing the custom domain** in Settings, under **API keys**), or ask the user to approve that scope for your app. See [Scopes](https://developers.helpcenter.io/content/scopes). | | `site_required` | 400 | An OAuth token reaches more than one help center, and the request didn't name one. `sites` lists the ids it can reach. An empty `sites` list means it reaches none. | Send `X-HCio-Site` or `?site=` with one of the ids. An empty list has two causes. The person's account owns no help center: connect your app from an account that owns the help center, or use an API key from that help center. Or every help center they shared has since been deleted: connect again and share one. | | `site_forbidden` | 403 | The help center you named isn't one this token reaches, or its id isn't a number. | Use an id from `GET /v1/sites`. | | `plan_upgrade_required` | 402 | A request through the MCP server for a help center that isn't on Growth or Catalyst. Adds `feature`, `upgrade_url` and `plans`. | Change the plan at `upgrade_url`. Calls you make to the REST API directly with an API key aren't affected. | | `notes_forbidden` | 403 | The person behind the credential can no longer see team notes on this help center, which takes the Owner, Admin, Editor or Translator role. | Use a credential of someone who has that access. | | `design_forbidden` | 403 | The person behind the credential can no longer edit this help center's design in the dashboard, which takes the Owner, Admin or Editor role. | Use a credential of someone who has that access. See [Design API](https://developers.helpcenter.io/content/design-api). | | `analytics_rate_limited` | 429 | More than 6 analytics requests in a minute with this credential. | Wait for `Retry-After` seconds, and store what you read: most figures change once a day. | | `analytics_unavailable` | 503 | The analytics are temporarily unavailable. Comes with `Retry-After: 300`. | Try again in five minutes. | | `category_not_empty` | 409 | You are deleting a category that still holds articles, drafts included, or child categories. Adds `articles_count` and `children_count`. | Send `on_orphan=uncategorize` or `on_orphan=reparent`. | | `upload_url_invalid` | 403 | The upload URL was changed, or isn't known. | Use the exact `upload_url`, or create another. | | `upload_url_revoked` | 403 | The credential that created the upload URL can no longer upload images to this help center. | Create another with a credential that can. | | `upload_url_expired` | 410 | The upload URL is past its `expires_at`, 15 minutes after it was created. | Create another. | | `upload_url_used` | 410 | The upload URL was already used. Each one takes a single upload. | Create another. | | `STAGING_UNAVAILABLE` | 403 | Staged changes aren't available on this help center: it isn't on the Catalyst plan. | Update the live article with `PATCH /v1/articles/{id}`, or upgrade to Catalyst. | | `NOT_PUBLISHED` | 422 | You staged changes on an article that isn't published, so there is no live version to keep them apart from. | Update the article with `PATCH /v1/articles/{id}`. | | `ARTICLE_UNPUBLISHED` | 422 | You staged changes on an article that was unpublished after changes were staged. Those staged changes are kept. | Publish the article again, or update it with `PATCH /v1/articles/{id}`. | | `STALE_ETAG` | 409 | The staged changes changed since you read them. Adds `scope` and the current `etag` or `shared_etag`. | Read them again, then retry with the new value. | | `BASE_CHANGED` | 409 | The live article changed after you staged your edits. Adds `locale`. | Read the article again, or publish with `"force": true` to overwrite the live version. | | `CHANGE_SETS_UNAVAILABLE` | 422 | You sent `change_set_id` while staging changes. | Leave it out. Add the article to a change set with `POST /v1/change-sets/{id}/items`. | | `template_migration_required` | 409 | The help center doesn't use the template editor yet, so the design API can't reach it. Adds `customizer_url`. | Ask the owner to switch it to the template editor, at `customizer_url`. | | `nothing_to_publish` | 409 | You published a design draft that is the same as the live design. | Change the draft first. | | `stale_draft` | 412 | The design draft changed since the `etag` you sent. Adds the current `etag`. | Read the draft again, make your change on top of it, and retry. | | `template_invalid` | 422 | An operation in a design batch couldn't be applied, so nothing was saved. `errors` lists each problem with its `operation`, `op`, `path` and `message`. | Fix the operation the error names, and send the batch again. | | `unknown_theme` | 404 | No theme with that key. The message lists the keys. | Use a key from `GET /v1/template/themes`. | | `component_not_found`, `not_found` | 404 | You read a component, script or custom CSS block that isn't in the design draft. | Read the current ids with `GET /v1/template`. | | `version_not_found` | 404 | No published version of the design with that id on this help center. | Use an id from `GET /v1/template/versions`. | | `role_forbidden` | 403 | An OAuth token whose person's role on the help center doesn't allow this action. Adds `role`. | Ask one of the help center's admins to change the person's role, or use a credential of someone who has it. | | `api_key_required` | 403 | An OAuth token on `/v1/redirects`, which takes only API keys. | Use an API key of the help center. | | `custom_domain_not_in_plan` | 403 | The help center's plan doesn't include a custom domain. Adds `upgrade_url`. | The owner upgrades on the billing page. | | `domain_taken` | 409 | Another help center already uses that domain. | Remove it there first, or contact support. See [Custom domain](https://developers.helpcenter.io/content/custom-domain-api). | | `custom_domain_busy` | 409 | Another change to the help center's domain is still running. Nothing was changed. | Retry shortly. See [Custom domain](https://developers.helpcenter.io/content/custom-domain-api). | | `custom_domain_limit_reached` | 429 | The help center has made 4 domain changes in the last month. Nothing was changed. Comes with `Retry-After`. | Wait for `Retry-After` seconds, or ask support. | | `import_running` | 409 | The import is queued or running, so it can't be changed, run or cancelled now. | Follow it with `GET /v1/imports/{importId}` and try again once the run ends. | | `import_cancelled` | 409 | The import was cancelled. | Scan its address again with `POST /v1/imports`. See [Imports](https://developers.helpcenter.io/content/imports-api). | | `not_importable` | 409 | You ran an import whose address the importer can't read (`importable` is `false`). | Move the content another way. | | `main_language_changed` | 409 | The help center's main language changed after the import wrote into it, so a run would write nothing. | Switch the main language back in Settings, under Localization. | | `credentials_not_accepted` | 422 | An AI agent's key sent credentials for an import. It imports public sources only. | Leave the credentials out; the owner can connect a private source from the dashboard. | | `host_busy`, `host_daily_limit` | 422 | `POST /v1/imports`: other help centers are importing from that site right now (at most 2 scans or runs at once), or it has had 20,000 of the importer's requests today. | Scan again in a few minutes, or after 00:00 UTC; or file a migration request. | | `needs_credentials` | 422 | The import source is private: a Zendesk help center behind sign-in, or a HelpDocs site that needs its API key. Adds `platform`. | Send the credentials the message names (not with an AI agent's key). | | `not_public`, `too_large`, `robots`, `unreachable`, `remote_error` | 422 | `POST /v1/imports` couldn't read the source: it isn't public, it is larger than the importer takes, its robots.txt keeps crawlers out, or it didn't answer as expected. The message says which. Adds `platform`. | Follow the message; HelpCenter.io's migration team can move such a source. | | `source_not_redirectable` | 422 | A redirect from that path would never be used: the help center answers the path itself. | Redirect another path. | | `target_not_found` | 422 | A redirect points at a page of the help center that doesn't exist now, or isn't published and public. | Point it at the page's current address. | | `external_target_forbidden` | 422 | A redirect points at another site, and the key isn't allowed to redirect to other sites. An AI agent's key never is. | Point it at a page of the help center. A redirect to another site needs a Read & write key that an Owner or Admin allowed to redirect to other sites, with **Allow redirects to other sites** in Settings, under **API keys**. | | `redirect_loop`, `too_many_hops` | 422 | The redirect would make a loop, or a chain of more than 3 redirects. | Point each redirect at the final page. See [Redirects](https://developers.helpcenter.io/content/redirects-api). | | `limit_reached` | 422, 429 | 422: an unclaimed help center is full (500 articles, 100 categories or 500 MB of images and files; `limit` says which), or an import doesn't fit (adds `room` and `articles_to_import`). 429: a limit for your address, such as creating help centers or filing migration requests without an account, with `Retry-After`; an IPv6 address counts with the rest of its /64. Most add `limit`, `max` and `migration_request_endpoint`. | See [Migrate a knowledge base as an AI agent](https://developers.helpcenter.io/content/migrate-a-knowledge-base-as-an-ai-agent). | | `name_blocked` | 422 | A help center created without an account was given a name that contains a protected brand name. | Use your user's own product or company name. | | `capacity_reached`, `busy` | 503 | No help center can be created without an account right now: as many are open as HelpCenter.io allows, or many are being created (`busy`, with `Retry-After: 5`). A migration request that needs more new help centers than can be held right now is refused as a whole, and nothing is filed. | Try again later, or file a migration request. For a migration request, try later or with fewer sources that need a new help center. | | `agent_not_allowed` | 403 | An AI agent's key asked for something outside what it serves: its help center's content (articles, categories, images, imports, staged changes, change sets), its design (`/v1/template`, `/v1/translations`) and, once its owner has activated a plan, its redirects and custom domain. Before the claim, every other credential is refused the same way. | Use only those endpoints. Everything else is your user's, with an API key they create in Settings, under **API keys**. | | `capability_locked` | 403 | An AI agent's key on redirects or the custom domain before the help center's owner has claimed it, verified their email address and activated a plan. Adds `capability`, `requires` and `next_step`. | Pass `next_step.instruction` on to the owner. | | `agent_scripts_forbidden` | 403 | An AI agent's key tried to change the design's scripts or HTML. Nothing was changed. Adds `changes`. | Leave those parts out; the help center's team makes them in the Template Editor. | | `agent_css_url_forbidden` | 422 | An AI agent's key tried to load a file in CSS, or a Hero background, from another host. Nothing was changed. The message names the address. | Use a path on the help center, a `data:` image, or an image stored with `POST /v1/images`. | | `migration_in_progress` | 409 | An AI agent's key wrote to a help center that an open migration request holds. Reads still work. | Wait for the request to end. | | `already_claimed` | 409 | An AI agent's key tried to delete, or file a migration request for, a help center its owner has claimed. | Nothing to do: it belongs to its owner. | | `source_unavailable` | 409 | A migration request named an `agent_key` whose help center was claimed, expired or entered another request while the request was filed. | File the request without that source. | | `expired` | 410 | An AI agent's key whose help center expired before anyone claimed it, and was deleted. Adds `site_id`. | Start again with `POST /v1/agent/help-centers`. | | `CHANGE_SETS_UNAVAILABLE` (reason) | 402 | Change sets aren't available on this help center: it isn't on Catalyst, or doesn't have early-preview access yet. | Change the plan. On Catalyst, request access: in the dashboard, click **Change sets**, then **Request access**. Retrying doesn't help. | | `CHANGE_SET_FULL` (reason) | 409 | The change set already holds 500 items. Items added before the limit stay added. | Put the rest in a second change set. | | `TOO_MANY_OPEN_CHANGE_SETS` (reason) | 409 | The help center already has 25 open change sets. | Publish or delete one first. | | `CHANGE_SET_NOT_OPEN` (reason) | 409 | The change set is being published, or was published, so its contents can't change. | Create another change set. | | `CHANGE_SET_NOT_SCHEDULABLE` (reason) | 409 | You sent `scheduled_for` for a change set that is `partially_published` or `failed`. A publish of it was already tried, so it can't wait for a schedule. | Publish it now, without `scheduled_for`, to finish the release. | For example, deleting a category that still holds an article: ``` { "status": "error", "code": "category_not_empty", "message": "This category still holds content. Re-send with ?on_orphan=uncategorize or ?on_orphan=reparent to say where it should go.", "articles_count": 1, "children_count": 0 } ``` ### Codes inside results Bulk responses report each failed item's code in `results[].error.code`. Change sets report their items' problems with a `reason`. | Where | Code | What it means | | --- | --- | --- | | `POST /v1/articles/bulk` | `invalid_item` | The item isn't an article object. | | `POST /v1/articles/bulk` | `validation_error` | The item failed validation. `error.errors` lists the problems by field. | | `POST /v1/articles/bulk` | `invalid_argument` | The item uses a language code your help center doesn't have. | | `POST /v1/articles/bulk` | `external_id_busy` | Another request is importing the same `external_id`. Retry the item shortly. | | `POST /v1/articles/bulk` | `persist_failed` | The article couldn't be saved. | | `POST /v1/articles/bulk` | `limit_reached` | An unclaimed help center is full. Stop, and file a migration request for the rest. | | `POST /v1/redirects/bulk` | `validation_error`, `source_not_redirectable`, `target_not_found`, `external_target_forbidden`, `redirect_loop`, `too_many_hops` | The redirect couldn't be saved, as for `POST /v1/redirects`. The other items are saved. | | `POST /v1/images/bulk` | `storage_failed` | The image couldn't be stored. Retry it. | | `POST /v1/images/bulk` | `import_failed` | The image at the `source_url` was refused or couldn't be downloaded. The message says why. | | Change set `readiness.blockers[].reason` | `conflict`, `article_gone`, `category_gone` | An item can't be published: the live article changed after staging, or the article or category was deleted. The change set can't publish until you fix or remove it. | | Change set `readiness.warnings[].reason` | `already_applied`, `locale_gap`, `staged_not_added` | Worth a look, but publishing isn't blocked. | | Change set `rejected[].reason`, when you add items | `not_found` | No article or category with that id on the help center. A published article with nothing staged is rejected with a sentence instead. | ## Errors outside the REST API The other surfaces answer in their own formats. ### OAuth The authorization server on `https://helpcenter.io` answers with `error` and `error_description`, as OAuth 2.1 defines. See [OAuth 2.1 for apps and AI clients](https://developers.helpcenter.io/content/oauth). | `error` | Status | When | | --- | --- | --- | | `invalid_request` | 400, 404 | A client sent no `code_challenge`, or a `code_challenge_method` other than `S256`, or a `code`, `redirect_uri`, `code_verifier` or `refresh_token` is missing or malformed (400). Dynamic client registration switched off (404). | | `invalid_client` | 401 | Client authentication failed, the client isn't known, or, at the authorization endpoint, the `redirect_uri` isn't one the client registered. | | `invalid_grant` | 400 | An authorization code or refresh token that is invalid, expired, already used, revoked or issued to another client; at the token endpoint, a `redirect_uri` that isn't the one the code was issued for; or a `code_verifier` that doesn't match the PKCE challenge. Using a code a second time also revokes the tokens its first use returned. | | `invalid_scope` | Redirect | The authorization asked for a scope that doesn't exist. | | `unsupported_grant_type` | 400 | A grant other than `authorization_code` or `refresh_token`. | | `access_denied` | Redirect | The person clicked **Deny** on the consent screen. | | `invalid_target` | 400 | A `resource` that isn't allowed, or that this grant didn't include. | | `invalid_redirect_uri` | 400 | Client registration with missing or invalid `redirect_uris`. | | `invalid_client_metadata` | 400 | Client registration with an unsupported `grant_types` or `token_endpoint_auth_method`. | | `temporarily_unavailable` | 429 | More than 10 client registration requests in an hour from one IP address, refused ones included. | `GET https://helpcenter.io/oauth/userinfo` answers a bad token with `401` and `{"error": {"code": "invalid_token", "message": …, "status": 401}}`. ### MCP server `https://mcp.helpcenter.io` refuses a request before any tool runs with an HTTP status, and for a credential problem also a `WWW-Authenticate` header. Errors from the API behind a tool, such as `plan_upgrade_required`, reach the agent as the tool's result, with an explanation. See [Connect an AI assistant to your help center](https://developers.helpcenter.io/content/connect-an-ai-assistant-to-your-help-center). | Status | When | | --- | --- | | `401`, JSON-RPC error `-32600` | No credential: sign in with OAuth, or send an API key as `Authorization: Bearer`. | | `401`, `error="invalid_token"` | The OAuth token was rejected, for example because it expired or wasn't issued for `https://mcp.helpcenter.io`. | | `403`, `error="insufficient_scope"` | The token lacks a permission the tool needs, such as `analytics.read`. Reconnect and approve it. | | `400`, JSON-RPC error `-32000` | An MCP protocol version the server doesn't support. | | `406`, `415` | The request lacks `Accept: application/json, text/event-stream` or `Content-Type: application/json`. | ### Public MCP server The read-only server at `https:///mcp` answers with JSON-RPC errors. See [The public MCP server of a help center](https://developers.helpcenter.io/content/public-mcp-server). | Error | When | | --- | --- | | `404` `{"error": "not_found"}` | The help center has no public MCP server: it isn't public or hasn't been published yet, is limited to certain IP addresses or to your app, hides from search engines, has the server switched off, or isn't live. | | `429`, JSON-RPC error `429` | More than 120 requests a minute from your address, or 1,200 for the help center. Wait for `Retry-After` seconds. | | `413` | A request body over 64 KiB. | | `-32700` | The body isn't valid JSON. | | `-32600` | A batch of messages (send one per request), or an unsupported `MCP-Protocol-Version` header. | | `-32601` | An unknown method. | | `-32602` | An unknown tool, or protocol parameters that don't match the request. Invalid tool arguments come back as a tool result with `isError` instead. | | `-32020`, `-32022` | The `MCP-Protocol-Version`, `Mcp-Method` or `Mcp-Name` headers don't match the body, or name an unsupported protocol version. | ### Widget and embeds The widget's requests to `https://embed.helpcenter.io` answer with `status`, `message` and `code`. See [Content Security Policy and allowed origins](https://developers.helpcenter.io/content/content-security-policy). | Code | Status | When | | --- | --- | --- | | `WIDGET_NOT_FOUND` | 410 | No widget has the `app_id` in your snippet. | | `SITE_AUTH_REQUIRED` | 403 | The help center is private, or public but not published yet, and the request had no valid JWT. A reader's JWT doesn't open a public help center that isn't published yet: only its team, signed in, can see it. See [Sign readers into the widget with a JWT](https://developers.helpcenter.io/content/widget-jwt). | | `SITE_IP_RESTRICTED` | 403 | The help center only opens from certain IP addresses, and this visitor's isn't one of them. | | `SITE_UNAVAILABLE` | 403 | The help center is unavailable, so FAQ sections show nothing. | | `SITE_NOT_PUBLISHED` | 403 | A FAQ section was asked for on a public help center that hasn't been published yet. It serves FAQ sections once its team publishes it. | ### Single sign-on Single sign-on returns no error codes. `/sso/jwt` answers in plain text: `400` `Missing jwt parameter.`, `401` `Invalid SSO token.` (a claim of the wrong type included), `403` `Single sign-on is switched off for this help center.`, or `500` `SSO is not properly configured.` Most other problems send the reader to the HelpCenter.io sign-in page. See [Troubleshoot single sign-on](https://developers.helpcenter.io/content/troubleshoot-sso). ## Troubleshooting | Symptom | Cause | Fix | | --- | --- | --- | | An HTML page where you expected JSON | The web servers in front of the API refused the request: a body over 32 MB, or more requests than they allow from your address. | Send smaller requests, or slow down. Check the status before you parse. | | "The title field is required." although you sent a title | The request had no `Content-Type: application/json`, so the API didn't read the body. | Add the header. | | `401` with a key that looks right | A header other than `apikey` (`api_key` and `X-API-Key` aren't read), a deleted key, or a key whose creator's HelpCenter.io account was deleted. | Send `apikey`, or create a key. | | `404` for an article you see in the dashboard | The key belongs to another help center, or the article is in the Trash. | Check `GET /v1/sites`, or restore the article. | | `422` "The include email field must be true or false." | `true` or `false` in a query string or form field. | Send `1` or `0`. | | `429` although you send fewer than 300 requests a minute | Single-article, staged, version and change set reads count against the 120 writes; exports, bulk requests and image uploads share a limit per IP address; the edge allows about 2 requests a second. | See [Rate limits](https://developers.helpcenter.io/content/rate-limits). | | `400` from `POST /v1/articles`, naming `title.en` or another language | A title that is empty or only spaces in one of the languages you sent. | Send text in every title you include; leave a language out to leave it alone. | | `403` `insufficient_scope` on `/v1/custom-domain` with a Read & write key | The key wasn't allowed to manage the custom domain. | Ask an Owner or Admin of the help center to tick **Allow managing the custom domain** for the key in Settings, under **API keys**. | | `422` `external_target_forbidden` on `/v1/redirects` | The `to` points at another site, and the key isn't allowed to redirect to other sites. | Point it at a page of the help center, or ask an Owner or Admin to tick **Allow redirects to other sites** for the key. | ## Related - [Requests and responses](https://developers.helpcenter.io/content/requests-responses-and-conventions) - [Rate limits](https://developers.helpcenter.io/content/rate-limits) - [Scopes](https://developers.helpcenter.io/content/scopes) - [Choose which help center a request acts on](https://developers.helpcenter.io/content/choosing-a-help-center)