{"openapi":"3.1.0","info":{"title":"HelpCenter.io API","version":"v1","termsOfService":"https://helpcenter.io/legal/terms","description":"Read and manage a HelpCenter.io help center: articles, categories, staged changes and change sets, comments, editorial notes, images, interface strings, webhooks, analytics, the v2 design, the custom domain, and the widget with its embeddable FAQ sections.\n\n## Authentication\n\nMost `/v1` routes take either of two credentials:\n\n- **API key**: the `apikey` header. A key belongs to one help center and always acts on it. A read key holds `content.read` and `analytics.read`; a write key adds `content.write`, `webhooks.manage` and `embed.manage`. A key holds `notes.*` or `design.*` only when it was issued with them, and `domain.manage` only when it is a write key one of the help center's Owners or Admins granted \"Allow managing the custom domain\" in Settings.\n- **OAuth 2.0 access token**: `Authorization: Bearer <token>`. A token acts for the person who connected the app, on the help centers of their account, within the scopes they approved and the role they hold on each help center. When the account has more than one help center, name the one to act on with the `X-HCio-Site` header (or the `site` query parameter); `GET /v1/sites` lists them.\n\nEach operation's `security` names the credential and the scope it needs. `GET /v1` and `GET /v1/account` accept any valid credential. These routes take something else:\n\n- `POST /v1/images/uploads/{intent}` takes none: its signed URL is the credential.\n- `POST /v1/agent/help-centers` and `POST /v1/agent/migration-requests` take none: an AI agent creates a help center, or files a migration request, without an account.\n- `GET /v1/agent/migration-requests/{id}` takes only that request's status token, the `status_token` in the `201` that filed it, as `Authorization: Bearer <status_token>`. It is not an API key or an OAuth token, and neither of those reads the status: they get `404`.\n- `/v1/agent/help-centers/{id}` and `/v1/agent/help-centers/{id}/migration-request` take only that help center's agent key: the `api_key` its creation returned, in the `apikey` header.\n- `/v1/redirects` takes an API key only: no OAuth scope covers it.\n\nBefore the claim, and after it if its user kept it, an AI agent's key reaches only its help center's content, design, status and setup routes, `GET /v1` and `GET /v1/sites`. Any other route, `GET /v1/account` included, answers `403` with `code: agent_not_allowed`.\n\n## Errors\n\nRefusals are JSON with `status` and `message`, and a machine-readable `code` where one applies. A request with no valid credential gets `401` with `status: unauthorized` and a `docs_url` to start from. An AI agent's key whose unclaimed help center expired gets `410` with `code: expired` instead, until 30 days after the help center was deleted. Validation failures answer `422` with `status: validation_error` and an `errors` map of field to messages, except on `GET /v1/articles` and `POST /v1/articles`, which answer `400` (see each operation).\n\n## Rate limits\n\nThe API-wide limits are per credential and per minute: by default 300 requests on every route, 120 on writes and 6 on analytics. Some routes add a tighter limit of their own, counted per client IP address and stated on the route. A refused request gets `429` with `Retry-After`."},"servers":[{"url":"https://api.helpcenter.io"}],"tags":[{"name":"Account","description":"The API itself, the help centers a credential can act on, its account, and a full export."},{"name":"Articles","description":"Articles: list, read, create, update, delete, bulk import, reader feedback."},{"name":"Categories","description":"Categories: list, create, update, delete and order, and the order of their articles."},{"name":"Staged changes","description":"Edits saved for review before they go live, published one article at a time, and its earlier versions."},{"name":"Change sets","description":"Groups of staged changes published together, now or on a schedule."},{"name":"Comments","description":"Reader comments on articles and their moderation."},{"name":"Notes","description":"The team's private editorial notes on articles. Readers never see these."},{"name":"Images","description":"Images to embed in article content."},{"name":"Translations","description":"Interface strings: the help center's own wording (buttons, headings), per language."},{"name":"Webhooks","description":"Signed notifications when articles or categories change."},{"name":"Analytics","description":"Daily analytics reports."},{"name":"Widget and FAQ sections","description":"The widget's install details and the websites allowed to embed the help center, and the embeddable FAQ sections a customer places on their own pages: create, change and delete them, and choose and order the FAQs a custom section lists. Everything here is live: there is no draft."},{"name":"Template","description":"The help center's v2 design: draft, publish, themes and versions."},{"name":"Agent help centers","description":"Help centers an AI agent creates with no account and hands its user to claim.","externalDocs":{"description":"Migrate a knowledge base as an AI agent","url":"https://developers.helpcenter.io/content/migrate-a-knowledge-base-as-an-ai-agent"}},{"name":"Imports","description":"Bring a knowledge base in from Zendesk, Intercom, Document360, Help Scout, Front or HelpDocs with HelpCenter.io's importer: scan, choose, run, follow, then map the old addresses to the new pages for redirects."},{"name":"Custom domain","description":"The help center's own domain: set it, read the DNS record to add and follow its certificate until it is live, remove it."},{"name":"Redirects","description":"Paths the help center would otherwise not find, sent on to its pages or other addresses."}],"paths":{"/v1":{"get":{"operationId":"getApiBanner","summary":"API banner","description":"Answers any valid credential with a fixed banner, so a client can confirm its credential authenticates. Carries no scope gate and resolves no help center.","tags":["Account"],"security":[{"apiKey":[]},{"oauth2":[]}],"responses":{"200":{"description":"The credential is valid.","content":{"application/json":{"schema":{"type":"object","required":["status","message"],"properties":{"status":{"const":"success"},"message":{"const":"This is the public API of HelpCenter.io"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanUpgradeRequired"},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/v1/account":{"get":{"operationId":"getAccount","summary":"Get the account, plan and capabilities behind the credential","description":"Any valid credential may call this; it has no scope gate, so an OAuth token holding only identity scopes can still learn what it is connected to. It never requires or refuses a help center (no 400 `site_required`, no 403 `site_forbidden`). On the OAuth lane `kb.capabilities` is judged against the person's role on the help center named by `X-HCio-Site` / `?site=` when that one is in the grant, else the only granted one, else any granted one; a grant that reaches no help center reports both capabilities as false.","tags":["Account"],"security":[{"apiKey":[]},{"oauth2":[]}],"parameters":[{"$ref":"#/components/parameters/XHCioSite","description":"Optional, OAuth lane only. Selects the help center `kb.capabilities` is evaluated on. Never refused here: a value outside the grant falls back to \"any granted help center\". Ignored for API keys."},{"$ref":"#/components/parameters/SiteQuery","description":"Optional, OAuth lane only; query-string alternative to `X-HCio-Site` (the header wins when both are sent). Selects the help center `kb.capabilities` is evaluated on and is never refused here. Ignored for API keys."}],"responses":{"200":{"description":"The account head, its plan entitlement and the help centers the credential reaches.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanUpgradeRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/v1/export":{"get":{"operationId":"exportContent","summary":"Export a full content snapshot of one help center","description":"For an integration's initial sync or a backup. The first page (no `cursor`, or `cursor=0`) carries every category, ordered by id; every later page carries `categories: []`. Articles (drafts and private ones included, bodies sanitized exactly as on GET /v1/articles) are ordered by id behind an id cursor: send `cursor.next` back as `cursor` until `cursor.has_more` is false. Throttled to 30 requests per minute per client IP, on top of the read limit. For an OAuth caller whose role lacks `content.share` (built-in Viewer, Translator), articles and categories the dashboard keeps from them are left out after the page is cut, so a page can hold fewer than `limit` articles while `has_more` is true.","tags":["Account"],"security":[{"apiKey":["content.read"]},{"oauth2":["content.read"]}],"parameters":[{"$ref":"#/components/parameters/XHCioSite"},{"$ref":"#/components/parameters/SiteQuery"},{"name":"cursor","in":"query","required":false,"description":"Return articles with an id greater than this. Omit (or send 0) for the first page, which is also the only page that includes `categories`. Use the previous page's `cursor.next`.","schema":{"type":["integer","null"],"minimum":0,"default":0}},{"name":"limit","in":"query","required":false,"description":"Maximum articles per page.","schema":{"type":["integer","null"],"minimum":1,"maximum":200,"default":100}}],"responses":{"200":{"description":"One page of the snapshot.","content":{"application/json":{"schema":{"type":"object","required":["status","snapshot","categories","articles","cursor"],"properties":{"status":{"const":"success"},"snapshot":{"type":"object","required":["generated_at","site_id","helpcenter_id"],"properties":{"generated_at":{"type":"string","format":"date-time","description":"When this page was generated (ISO 8601 with offset)."},"site_id":{"type":"integer","description":"The help center exported."},"helpcenter_id":{"type":"string","format":"uuid","description":"The help center's stable public identifier (the `uuid` of GET /v1/sites)."}}},"categories":{"type":"array","description":"Every live category, ordered by id, on the first page only; an empty array on every page requested with a `cursor` greater than 0.","items":{"$ref":"#/components/schemas/Category"}},"articles":{"type":"array","description":"Live (non-trashed) articles of every status and visibility with an id above `cursor`, ordered by id ascending.","items":{"$ref":"#/components/schemas/Article"}},"cursor":{"type":"object","required":["next","has_more"],"properties":{"next":{"type":["integer","null"],"description":"The value to send as `cursor` for the next page (the id of the last article this page's query read); null when `has_more` is false."},"has_more":{"type":"boolean","description":"Whether articles with a higher id remain."}}}}}}}},"400":{"$ref":"#/components/responses/SiteRequired"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanUpgradeRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound","description":"The help center behind this API key has been deleted."},"422":{"$ref":"#/components/responses/ValidationFailed"},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/v1/sites":{"get":{"operationId":"listSites","summary":"List the help centers this credential can act on","description":"Discovery for the OAuth lane: every help center the connection is granted (owned by the account, with the user on its team, and ticked for this app on the consent screen), ordered by id; use an `id` as `X-HCio-Site` / `?site=` on other routes. An API key gets exactly its own help center, or an empty list if that help center has been deleted. Not paginated, and the listing itself ignores `X-HCio-Site` / `?site=`.","tags":["Account"],"security":[{"apiKey":["content.read"]},{"oauth2":["content.read"]}],"responses":{"200":{"description":"The help centers, ordered by id.","content":{"application/json":{"schema":{"type":"object","required":["status","sites","meta"],"properties":{"status":{"const":"success"},"sites":{"type":"array","items":{"$ref":"#/components/schemas/Site"}},"meta":{"type":"object","required":["items_count"],"properties":{"items_count":{"type":"integer","minimum":0,"description":"Number of entries in `sites`."}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanUpgradeRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/v1/articles":{"get":{"operationId":"listArticles","summary":"List articles","description":"Lists the help center's articles of every status and visibility, ordered by `updated_at` descending (ties broken by id). With `search`, only published articles that an anonymous visitor of the help center could find are matched, ranked by relevance unless `order_by` is given, from the search engine's top 1000 hits; if the search engine fails, the endpoint falls back to a database title/content match over the 30 most recently updated matches, in recency order. On the OAuth lane, a Viewer or Translator does not see articles (or category ids) the dashboard keeps from them, and `meta` counts only what they may see.","tags":["Articles"],"security":[{"apiKey":["content.read"]},{"oauth2":["content.read"]}],"parameters":[{"$ref":"#/components/parameters/XHCioSite"},{"$ref":"#/components/parameters/SiteQuery"},{"name":"limit","in":"query","required":false,"description":"Articles per page. Out of range is a 400, not clamped.","schema":{"type":"integer","minimum":1,"maximum":100,"default":100}},{"name":"page","in":"query","required":false,"description":"1-based page number. A page past the last returns an empty `articles` array.","schema":{"type":"integer","minimum":1,"default":1}},{"name":"category_id","in":"query","required":false,"description":"Only articles whose `category_id` is this id or whose `categories` list contains it. Must be one of this help center's category ids (else 400); `0` is accepted and applies no filter.","schema":{"type":"integer"}},{"name":"search","in":"query","required":false,"description":"Full-text query. Restricts results to published, publicly visible articles (see the operation description). An empty value lists as if absent.","schema":{"type":"string"}},{"name":"lang","in":"query","required":false,"description":"Language to search in; read only together with a non-empty `search` (ignored on a plain listing). Must be the help center's default language or one it has enabled, else 422. For a non-default language only articles whose translation is released are matched. Defaults to the help center's default language.","schema":{"type":"string"}},{"name":"status","in":"query","required":false,"description":"Only articles in this status. Combined with `search` (which already restricts to published), `draft` returns nothing.","schema":{"type":"string","enum":["published","draft"]}},{"name":"updated_since","in":"query","required":false,"description":"Only articles with `updated_at` at or after this moment. Any date string Laravel's `date` rule accepts (parsed by PHP's strtotime).","schema":{"type":"string"}},{"name":"order_by","in":"query","required":false,"description":"Sort by the stored view count instead of the default (`updated_at`, or search relevance when `search` is given).","schema":{"type":"string","enum":["views"]}},{"name":"order_type","in":"query","required":false,"description":"Sort direction. Ignored when results are in search-relevance order.","schema":{"type":"string","enum":["asc","desc"],"default":"desc"}}],"responses":{"200":{"description":"One page of articles.","content":{"application/json":{"schema":{"type":"object","required":["status","articles","meta"],"properties":{"status":{"const":"success"},"articles":{"type":"array","items":{"$ref":"#/components/schemas/Article"}},"meta":{"type":"object","required":["page","per_page","total_pages","items_count"],"properties":{"page":{"type":"integer","description":"The page served (echoes `page`)."},"per_page":{"type":"integer","description":"The page size used (echoes `limit`, default 100)."},"total_pages":{"type":"integer","minimum":1,"description":"ceil(items_count / per_page), and 1 when there are no matches."},"items_count":{"type":"integer","description":"Total matching articles across all pages."}}}}}}}},"400":{"description":"Either `site_required` (an OAuth credential spanning several help centers named none), or a query parameter failed validation. This endpoint answers parameter validation with 400 and a `{status: \"validation_error\", errors}` body that has no `message` (the `lang` check is the exception: 422).","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/SiteRequiredError"},{"$ref":"#/components/schemas/ArticlesListValidationError"}]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanUpgradeRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/ValidationFailed"},"429":{"$ref":"#/components/responses/TooManyRequests"}}},"post":{"operationId":"createArticle","summary":"Create an article (or upsert by external_id)","description":"Creates an article (201, `action: \"created\"`). When `external_id` already keys an existing article on this help center, that article is updated instead (200, `action: \"updated\"`), under the same rules as the update path. Field validation failures answer 400 with a bare `{field: [messages]}` map, not the `validation_error` envelope. Creating as published, filing into categories, setting visibility, and upserting onto a published article each need their own capability on the OAuth lane. Takes optional `change_note` and `change_refs`: what you say about this change, kept with the activity record of the article it creates or updates and never public. A bad value answers 400 like any other field. `POST /v1/articles/bulk` does not take them.","tags":["Articles"],"security":[{"apiKey":["content.write"]},{"oauth2":["content.write"]}],"parameters":[{"$ref":"#/components/parameters/XHCioSite"},{"$ref":"#/components/parameters/SiteQuery"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/ArticleCreateRequest"},{"$ref":"#/components/schemas/ChangeNoteFields"}]}}}},"responses":{"200":{"description":"`external_id` matched an existing article, which was updated (`action` is `updated`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ArticleWriteResult"}}}},"201":{"description":"A new article was created (`action` is `created`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ArticleWriteResult"}}}},"400":{"description":"One of: `site_required`; a field failed validation (bare map of field path to messages, e.g. keys `title`, `title.en`, `metadata.title.en`); or `{status: \"error\", message}` for a language key that is not one of the help center's languages (`Unsupported language key \"xx\" provided.`) or a slug that would exceed 191 characters, or whose current value is longer than 191 characters and cannot be redirected from. A bad `change_note` or `change_refs` fails here too, under its own field path (`change_note`, `change_refs`, `change_refs.0.url`).","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/SiteRequiredError"},{"$ref":"#/components/schemas/ArticlesFieldErrors"},{"$ref":"#/components/schemas/Error"}]},"examples":{"referenceNotHttps":{"summary":"A reference that is not an https address","value":{"change_refs.0.url":["The change refs.0.url must start with https://."]}},"referenceWithCredentials":{"summary":"A reference with a user name in the address","value":{"change_refs.1.url":["The change refs.1.url must not contain a user name or password."]}},"noteTooLong":{"summary":"A note longer than 2,000 characters","value":{"change_note":["The change note may not be longer than 2,000 characters."]}},"tooManyReferences":{"summary":"More than 5 references","value":{"change_refs":["A change can carry at most 5 references."]}},"referenceShape":{"summary":"A reference with a key other than label and url","value":{"change_refs.0":["Each reference must be an object with only a label and a url."]}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanUpgradeRequired"},"403":{"description":"Refused. `insufficient_scope`, `role_forbidden` and `site_forbidden` as on every scoped route; a credential without write access gets `{status, message}` with no code (`You are not allowed to create articles`). `capability_forbidden` names the missing capability: `articles.create` (always required); `articles.publish` for `published: true`; `categories.manage` when `category_id` or a non-empty `categories` is sent; `content.share` when `visibility` is sent or the `external_id` belongs to an article private to the caller; and, when the upsert updates an existing article, `articles.publish` / `articles.unpublish` if `published` would change its status. `publication_forbidden` when the `external_id` resolves to a published article and the caller cannot publish.","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/Error"},{"$ref":"#/components/schemas/InsufficientScopeError"},{"$ref":"#/components/schemas/RoleForbiddenError"},{"$ref":"#/components/schemas/SiteForbiddenError"},{"$ref":"#/components/schemas/CapabilityForbiddenError"},{"$ref":"#/components/schemas/ArticlesPublicationForbiddenError"}]}}}},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"Another request is upserting the same `external_id` on this help center and the per-key lock could not be taken within the wait (10 seconds by default). Retry shortly; nothing was written.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"`limit_reached`: this help center is unclaimed and already holds 500 articles (drafts and articles in Trash count), or (`limit`: `storage_bytes`) an image `rehost_images` copies does not fit in the 500 MB of images and files it may store. Nothing of the article was written (images copied before the one that did not fit stay, and a retry reuses them); file a migration request, or have the help center claimed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LimitReachedError"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/v1/articles/bulk":{"post":{"operationId":"bulkUpsertArticles","summary":"Create or upsert up to 50 articles","description":"Runs each item through the same validation, authorization and create-or-upsert logic as `POST /v1/articles`, one at a time, and reports each outcome in `results` (request order). Answers 207 whenever the envelope is valid: a failing item never discards the others, and image re-hosting shares one time budget across the batch. Rate-limited by `throttle:20,1` (20 requests per minute, counted per client IP address rather than per credential) instead of the write limiter, on top of the group's read ceiling.","tags":["Articles"],"security":[{"apiKey":["content.write"]},{"oauth2":["content.write"]}],"parameters":[{"$ref":"#/components/parameters/XHCioSite"},{"$ref":"#/components/parameters/SiteQuery"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["articles"],"properties":{"articles":{"type":"array","minItems":1,"maxItems":50,"description":"The articles to create or upsert. Items are validated one by one: an item that is not an object, or fails the create rules, fails alone in `results`.","items":{"$ref":"#/components/schemas/ArticleCreateRequest"}}}}}}},"responses":{"207":{"description":"Per-item outcomes.","content":{"application/json":{"schema":{"type":"object","required":["status","results","meta"],"properties":{"status":{"const":"success","description":"Always `success`, even when every item failed; read `meta.failed` and each result."},"results":{"type":"array","items":{"$ref":"#/components/schemas/ArticleBulkItemResult"}},"meta":{"type":"object","required":["total","created","updated","failed"],"properties":{"total":{"type":"integer","description":"Number of entries in `results` (one per item sent)."},"created":{"type":"integer"},"updated":{"type":"integer"},"failed":{"type":"integer"}}}}}}}},"400":{"$ref":"#/components/responses/SiteRequired"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanUpgradeRequired"},"403":{"description":"The whole batch was refused. `insufficient_scope`, `role_forbidden`, `site_forbidden` as on every scoped route; `{status, message}` with no code for a credential without write access (`You are not allowed to create articles`); `capability_forbidden` with `capability: \"articles.create\"`. Per-item refusals (publishing, categories, sharing, published upserts) are reported inside the 207 instead.","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/Error"},{"$ref":"#/components/schemas/InsufficientScopeError"},{"$ref":"#/components/schemas/RoleForbiddenError"},{"$ref":"#/components/schemas/SiteForbiddenError"},{"$ref":"#/components/schemas/CapabilityForbiddenError"}]}}}},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/ValidationFailed"},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/v1/articles/{articleId}":{"get":{"operationId":"getArticle","summary":"Get an article","description":"Returns one article of the resolved help center. An id belonging to another help center, a missing or trashed id, and (for an OAuth Viewer or Translator) a private article kept from the caller all answer the same 404. This read also counts against the write rate limiter (`public-api-write`), not only the read one.","tags":["Articles"],"security":[{"apiKey":["content.read"]},{"oauth2":["content.read"]}],"parameters":[{"name":"articleId","in":"path","required":true,"description":"The article's id.","schema":{"type":"integer"}},{"$ref":"#/components/parameters/XHCioSite"},{"$ref":"#/components/parameters/SiteQuery"}],"responses":{"200":{"description":"The article.","content":{"application/json":{"schema":{"type":"object","required":["status","article"],"properties":{"status":{"const":"success"},"article":{"$ref":"#/components/schemas/Article"}}}}}},"400":{"$ref":"#/components/responses/SiteRequired"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanUpgradeRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"}}},"patch":{"operationId":"updateArticle","summary":"Update an article (live)","description":"Updates the live article: on a published article the change reaches readers at once, so a caller that cannot publish is refused with `publication_forbidden` and pointed at `PATCH /v1/articles/{articleId}/staged`. Fields not sent are left alone and per-language maps merge by language. Sending `external_id` adopts the article under that key, replacing any key it had. The body is validated before the article is looked up, so an invalid body answers 422 even for an unknown id. Takes optional `change_note` and `change_refs`: what you say about this change, kept with its activity record and never public.","tags":["Articles"],"security":[{"apiKey":["content.write"]},{"oauth2":["content.write"]}],"parameters":[{"name":"articleId","in":"path","required":true,"description":"The article's id.","schema":{"type":"integer"}},{"$ref":"#/components/parameters/XHCioSite"},{"$ref":"#/components/parameters/SiteQuery"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/ArticleWriteFields"},{"$ref":"#/components/schemas/ChangeNoteFields"},{"$ref":"#/components/schemas/ArticleWriteNoteFields"}]}}}},"responses":{"200":{"description":"The updated article.","content":{"application/json":{"schema":{"type":"object","required":["status","article"],"properties":{"status":{"const":"success"},"article":{"$ref":"#/components/schemas/Article"},"images":{"$ref":"#/components/schemas/ArticleImageImportReport","description":"Present only when image re-hosting ran: it was enabled for this request and `content` was sent."},"notes":{"$ref":"#/components/schemas/CreatedArticleNotes"},"annotate_changes":{"$ref":"#/components/schemas/ArticleChangeAnnotations"}}}}}},"400":{"description":"`site_required`, or `{status: \"error\", message}` for a language key that is not one of the help center's languages (`Unsupported language key \"xx\" provided.`) or a slug that would exceed 191 characters, or whose current value is longer than 191 characters and cannot be redirected from. Nothing is written.","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/SiteRequiredError"},{"$ref":"#/components/schemas/Error"}]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanUpgradeRequired"},"403":{"description":"Refused. `insufficient_scope`, `role_forbidden`, `site_forbidden` as on every scoped route; `{status, message}` with no code for a credential without write access (`You are not allowed to update articles`). `capability_forbidden` names the missing capability: `articles.edit` (always required); `articles.publish` when `published: true` would publish a draft; `articles.unpublish` when `published: false` would unpublish; `categories.manage` when `category_id` or a non-empty `categories` is sent; `content.share` when `visibility` is sent. `publication_forbidden` (with `staged_endpoint`) when the article is published and the caller cannot publish. With `notes` or `annotate_changes`: also `insufficient_scope` / `role_forbidden` for `notes.write`, or `notes_forbidden`, exactly as the notes routes answer them, before anything is written.","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/Error"},{"$ref":"#/components/schemas/InsufficientScopeError"},{"$ref":"#/components/schemas/RoleForbiddenError"},{"$ref":"#/components/schemas/SiteForbiddenError"},{"$ref":"#/components/schemas/CapabilityForbiddenError"},{"$ref":"#/components/schemas/ArticlesPublicationForbiddenError"},{"$ref":"#/components/schemas/NotesForbiddenError"}]}}}},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"The `external_id` sent already belongs to another existing article on this help center (`conflicting_article_id` names it), or another request holds that key's lock (plain `{status, message}`). Nothing is written.","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/ArticlesExternalIdConflictError"},{"$ref":"#/components/schemas/Error"}]}}}},"422":{"description":"The request failed validation. `errors` maps each failing field to its messages. Or `limit_reached` (`limit`: `storage_bytes`): the help center is unclaimed and an image `rehost_images` copies does not fit in the 500 MB of images and files it may store; the article was not changed. Or a note that cannot be placed (`note_quote_not_found`, `note_quote_ambiguous`, `note_lang_required`, `note_locale_not_written`, `note_quotes_overlap`, `annotate_changes_covered`): nothing was written.","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/ValidationError"},{"$ref":"#/components/schemas/LimitReachedError"},{"$ref":"#/components/schemas/NoteRefusedError"}]},"examples":{"referenceNotHttps":{"summary":"A reference that is not an https address","value":{"status":"validation_error","message":"The request could not be accepted.","errors":{"change_refs.0.url":["The change refs.0.url must start with https://."]}}},"referenceWithCredentials":{"summary":"A reference with a user name in the address","value":{"status":"validation_error","message":"The request could not be accepted.","errors":{"change_refs.1.url":["The change refs.1.url must not contain a user name or password."]}}},"noteTooLong":{"summary":"A note longer than 2,000 characters","value":{"status":"validation_error","message":"The request could not be accepted.","errors":{"change_note":["The change note may not be longer than 2,000 characters."]}}},"tooManyReferences":{"summary":"More than 5 references","value":{"status":"validation_error","message":"The request could not be accepted.","errors":{"change_refs":["A change can carry at most 5 references."]}}},"referenceShape":{"summary":"A reference with a key other than label and url","value":{"status":"validation_error","message":"The request could not be accepted.","errors":{"change_refs.0":["Each reference must be an object with only a label and a url."]}}}}}}},"429":{"$ref":"#/components/responses/TooManyRequests"}}},"delete":{"operationId":"deleteArticle","summary":"Move an article to Trash","description":"Soft-deletes the article: it moves to Trash, where it stays restorable from the dashboard, and leaves the help center and search. Deleting a published article also takes it offline, so it additionally needs the `articles.unpublish` capability.","tags":["Articles"],"security":[{"apiKey":["content.write"]},{"oauth2":["content.write"]}],"parameters":[{"name":"articleId","in":"path","required":true,"description":"The article's id.","schema":{"type":"integer"}},{"$ref":"#/components/parameters/XHCioSite"},{"$ref":"#/components/parameters/SiteQuery"}],"responses":{"200":{"description":"The article is in Trash.","content":{"application/json":{"schema":{"type":"object","required":["status","message","article"],"properties":{"status":{"const":"success"},"message":{"const":"Article moved to trash."},"article":{"type":"object","required":["id","deleted_at"],"properties":{"id":{"type":"integer"},"deleted_at":{"type":"string","format":"date-time","description":"ISO 8601 with offset."}}}}}}}},"400":{"$ref":"#/components/responses/SiteRequired"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanUpgradeRequired"},"403":{"description":"Refused. `insufficient_scope`, `role_forbidden`, `site_forbidden` as on every scoped route; `{status, message}` with no code for a credential without write access (`You are not allowed to delete articles`); `capability_forbidden` with `capability` `articles.delete` (always required) or `articles.unpublish` (the article is published).","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/Error"},{"$ref":"#/components/schemas/InsufficientScopeError"},{"$ref":"#/components/schemas/RoleForbiddenError"},{"$ref":"#/components/schemas/SiteForbiddenError"},{"$ref":"#/components/schemas/CapabilityForbiddenError"}]}}}},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/v1/articles/{articleId}/feedback":{"post":{"operationId":"createArticleFeedback","summary":"Record a reader's vote on an article","description":"Relays a reader's vote into the same ratings the help center's own buttons write: one vote per article, voter and language, and a repeat vote replaces it (200 instead of 201). The voter is `visitor_key` when sent (stored only as a hash), otherwise the calling server's IP address, so an integration relaying for many readers should send `visitor_key`. A `lang` the help center does not have is recorded as its default language.","tags":["Articles"],"security":[{"apiKey":["content.write"]},{"oauth2":["content.write"]}],"parameters":[{"name":"articleId","in":"path","required":true,"description":"The article's id.","schema":{"type":"integer"}},{"$ref":"#/components/parameters/XHCioSite"},{"$ref":"#/components/parameters/SiteQuery"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["vote"],"properties":{"vote":{"type":"string","enum":["helpful","not_helpful","love"],"description":"Stored as score 1, -1 and 2 respectively."},"lang":{"type":["string","null"],"maxLength":10,"description":"Language the reader voted in. Defaults to, and falls back to, the help center's default language."},"message":{"type":["string","null"],"maxLength":1000,"description":"Optional free-text comment stored with the vote; omitting it clears any message on a replaced vote."},"visitor_key":{"type":["string","null"],"maxLength":128,"description":"Any stable opaque token for the reader, used to de-duplicate their votes. Trimmed and stored as a hash. When absent or blank, the caller's IP address is the voter."}}}}}},"responses":{"200":{"description":"This voter's existing vote for the article and language was replaced.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ArticleFeedbackResult"}}}},"201":{"description":"A new vote was recorded.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ArticleFeedbackResult"}}}},"400":{"$ref":"#/components/responses/SiteRequired"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanUpgradeRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/ValidationFailed"},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/v1/categories":{"get":{"operationId":"listCategories","summary":"List categories","description":"Every live category of the help center, of every privacy level, paginated. The list is not sorted by `position` (no order is applied); rebuild the tree from `parent` and `position`. For an OAuth caller whose role lacks `content.share` (built-in Viewer, Translator), `private` categories not shared with them are left out of both the list and the counts.","tags":["Categories"],"security":[{"apiKey":["content.read"]},{"oauth2":["content.read"]}],"parameters":[{"$ref":"#/components/parameters/XHCioSite"},{"$ref":"#/components/parameters/SiteQuery"},{"name":"limit","in":"query","required":false,"description":"Categories per page.","schema":{"type":["integer","null"],"minimum":1,"maximum":100,"default":100}},{"name":"page","in":"query","required":false,"description":"1-based page number. A page past the end returns an empty `categories` array.","schema":{"type":["integer","null"],"minimum":1,"default":1}}],"responses":{"200":{"description":"One page of categories.","content":{"application/json":{"schema":{"type":"object","required":["status","categories","meta"],"properties":{"status":{"const":"success"},"categories":{"type":"array","items":{"$ref":"#/components/schemas/Category"}},"meta":{"type":"object","required":["page","per_page","total_pages","items_count"],"properties":{"page":{"type":"integer","minimum":1},"per_page":{"type":"integer","minimum":1,"maximum":100,"description":"The `limit` applied."},"total_pages":{"type":"integer","minimum":1,"description":"ceil(items_count / per_page), and 1 when there are no categories."},"items_count":{"type":"integer","minimum":0,"description":"Total categories across all pages."}}}}}}}},"400":{"$ref":"#/components/responses/SiteRequired"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanUpgradeRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound","description":"The help center behind this API key has been deleted."},"422":{"$ref":"#/components/responses/ValidationFailed"},"429":{"$ref":"#/components/responses/TooManyRequests"}}},"post":{"operationId":"createCategory","summary":"Create a category","description":"Creates a `public` category (privacy cannot be set over the API) and attaches the acting user as its owner, as the dashboard does. Visible on the help center at once: taxonomy has no staged copy, so on the OAuth lane the person needs both `categories.manage` and `articles.publish`. Write-limited.","tags":["Categories"],"security":[{"apiKey":["content.write"]},{"oauth2":["content.write"]}],"parameters":[{"$ref":"#/components/parameters/XHCioSite"},{"$ref":"#/components/parameters/SiteQuery"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"object","minProperties":1,"description":"The name as a `{locale: text}` map. Every key must be a language HelpCenter.io supports AND one enabled on this help center (its default language or an additional one). Values are trimmed and must be non-empty.","additionalProperties":{"type":"string","minLength":1,"maxLength":190}},"description":{"type":["object","null"],"description":"The description as a `{locale: text}` map, keyed like `name`. A null or empty value stores an empty description for that locale.","additionalProperties":{"type":["string","null"],"maxLength":65535}},"parent_id":{"type":["integer","null"],"description":"Id of a live category of this help center to nest under. 0, null or omitted makes a top-level category. Another help center's category, a trashed one and the article sentinel -1 are refused with 422."},"icon":{"type":["string","null"],"maxLength":190,"description":"Stored as the category's image URL and returned as `icon`. Not validated as a URL."},"position":{"type":["integer","null"],"minimum":0,"maximum":2147483647,"description":"Sibling order (ascending, compared only within the same parent). Omitted or null stores the column default 100000, which sorts after every explicitly positioned sibling."}}}}}},"responses":{"201":{"description":"Created. When `position` was omitted, `category.position` in this response reads 0 although 100000 was stored (the model is not re-read after insert); GET /v1/categories shows the stored value.","content":{"application/json":{"schema":{"type":"object","required":["status","category"],"properties":{"status":{"const":"success"},"category":{"$ref":"#/components/schemas/Category"}}}}}},"400":{"$ref":"#/components/responses/SiteRequired"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanUpgradeRequired"},"403":{"description":"Refused. Codes: `insufficient_scope` (the credential lacks content.write), `role_forbidden` (OAuth: the person's role does not allow content.write), `site_forbidden` (OAuth: `X-HCio-Site` / `?site=` names a help center outside the grant), `capability_forbidden` (OAuth only: the role lacks `categories.manage`, checked first, or `articles.publish`), or no code with the message \"You are not allowed to create categories.\" from the form request.","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/InsufficientScopeError"},{"$ref":"#/components/schemas/RoleForbiddenError"},{"$ref":"#/components/schemas/SiteForbiddenError"},{"$ref":"#/components/schemas/Error"},{"allOf":[{"$ref":"#/components/schemas/CapabilityForbiddenError"},{"type":"object","properties":{"capability":{"enum":["categories.manage","articles.publish"]}}}]}]}}}},"404":{"$ref":"#/components/responses/NotFound","description":"The help center behind this API key has been deleted."},"422":{"description":"The request failed validation (`validation_error`, with `errors`), or `limit_reached`: this help center is unclaimed and already holds 100 categories (those in Trash count).","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/ValidationError"},{"$ref":"#/components/schemas/LimitReachedError"}]}}}},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/v1/categories/order":{"put":{"operationId":"reorderCategories","summary":"Reorder categories in one call","description":"Sets `position` on each listed category. Positions order siblings, so one payload may mix tree levels and each category sorts within its own parent; it does not move categories between parents. Categories not listed are left alone. Needs `categories.manage` and `articles.publish` on the OAuth lane. Write-limited.","tags":["Categories"],"security":[{"apiKey":["content.write"]},{"oauth2":["content.write"]}],"parameters":[{"$ref":"#/components/parameters/XHCioSite"},{"$ref":"#/components/parameters/SiteQuery"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["categories"],"properties":{"categories":{"type":"array","minItems":1,"maxItems":500,"description":"The order to apply. If an id appears twice, its last entry wins.","items":{"type":"object","required":["id"],"properties":{"id":{"type":"integer","description":"A live category of this help center; any other id fails the whole request with 422."},"position":{"type":["integer","null"],"minimum":0,"maximum":2147483647,"description":"Omitted or null: the item's 0-based index in `categories`."}}}}}}}}},"responses":{"200":{"description":"Positions written.","content":{"application/json":{"schema":{"type":"object","required":["status","message","updated_count","categories"],"properties":{"status":{"const":"success"},"message":{"const":"Categories reordered."},"updated_count":{"type":"integer","minimum":0,"description":"Distinct categories written."},"categories":{"type":"array","description":"The categories written, sorted by their new `position` ascending.","items":{"$ref":"#/components/schemas/Category"}}}}}}},"400":{"$ref":"#/components/responses/SiteRequired"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanUpgradeRequired"},"403":{"description":"Refused. Codes: `insufficient_scope`, `role_forbidden` (OAuth), `site_forbidden` (OAuth), `capability_forbidden` (OAuth only: the role lacks `categories.manage`, checked first, or `articles.publish`), or no code with the message \"You are not allowed to reorder categories.\" from the form request.","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/InsufficientScopeError"},{"$ref":"#/components/schemas/RoleForbiddenError"},{"$ref":"#/components/schemas/SiteForbiddenError"},{"$ref":"#/components/schemas/Error"},{"allOf":[{"$ref":"#/components/schemas/CapabilityForbiddenError"},{"type":"object","properties":{"capability":{"enum":["categories.manage","articles.publish"]}}}]}]}}}},"404":{"$ref":"#/components/responses/NotFound","description":"The help center behind this API key has been deleted."},"422":{"$ref":"#/components/responses/ValidationFailed"},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/v1/categories/{categoryId}":{"patch":{"operationId":"updateCategory","summary":"Update a category","description":"Every field is optional and only fields present are written; translatable maps merge per locale, so locales not sent are kept. Visible on the help center at once; needs `categories.manage` and `articles.publish` on the OAuth lane. Privacy cannot be changed over the API. Write-limited.","tags":["Categories"],"security":[{"apiKey":["content.write"]},{"oauth2":["content.write"]}],"parameters":[{"$ref":"#/components/parameters/XHCioSite"},{"$ref":"#/components/parameters/SiteQuery"},{"name":"categoryId","in":"path","required":true,"description":"The category's id. The route does not constrain the format; it is looked up among this help center's live categories, and anything else (another help center's category included) answers 404.","schema":{"type":"string"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"object","minProperties":1,"description":"`{locale: text}` map; each key must be a supported language enabled on this help center. Values are trimmed and must be non-empty. Null is refused.","additionalProperties":{"type":"string","minLength":1,"maxLength":190}},"description":{"type":"object","description":"`{locale: text}` map keyed like `name`; a null or empty value clears that locale's description. Null for the whole field is refused.","additionalProperties":{"type":["string","null"],"maxLength":65535}},"parent_id":{"type":["integer","null"],"description":"0 or null makes the category top-level; otherwise a live category of this help center. Refused with 422: another help center's or a trashed category, the sentinel -1, the category itself, or one of its descendants."},"icon":{"type":["string","null"],"maxLength":190,"description":"Image URL returned as `icon`; null clears it. Not validated as a URL."},"position":{"type":"integer","minimum":0,"maximum":2147483647,"description":"Sibling order; 0 puts it first. Null is refused."}}}}}},"responses":{"200":{"description":"Updated.","content":{"application/json":{"schema":{"type":"object","required":["status","category"],"properties":{"status":{"const":"success"},"category":{"$ref":"#/components/schemas/Category"}}}}}},"400":{"$ref":"#/components/responses/SiteRequired"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanUpgradeRequired"},"403":{"description":"Refused. Codes: `insufficient_scope`, `role_forbidden` (OAuth), `site_forbidden` (OAuth), `capability_forbidden` (OAuth only: the role lacks `categories.manage`, checked first, or `articles.publish`), or no code with the message \"You are not allowed to update categories.\" from the form request.","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/InsufficientScopeError"},{"$ref":"#/components/schemas/RoleForbiddenError"},{"$ref":"#/components/schemas/SiteForbiddenError"},{"$ref":"#/components/schemas/Error"},{"allOf":[{"$ref":"#/components/schemas/CapabilityForbiddenError"},{"type":"object","properties":{"capability":{"enum":["categories.manage","articles.publish"]}}}]}]}}}},"404":{"$ref":"#/components/responses/NotFound","description":"`Category not found.` for a missing, trashed or other help center's category (the same body either way), or the help center behind this API key has been deleted."},"422":{"$ref":"#/components/responses/ValidationFailed"},"429":{"$ref":"#/components/responses/TooManyRequests"}}},"delete":{"operationId":"deleteCategory","summary":"Delete a category","description":"Moves the category to the trash, with an explicit policy for its content. `refuse` (default) answers 409 if the category holds any live article or sub-category. `uncategorize` files its articles as Uncategorized (multi-category articles keep their other categories) and makes its sub-categories top-level. `reparent` moves both up to the deleted category's own parent, which falls back to `uncategorize` when there is no live parent on this help center. Trashed articles and sub-categories never cause a refusal but are re-filed too. Needs `categories.manage` and `articles.publish` on the OAuth lane. Write-limited.","tags":["Categories"],"security":[{"apiKey":["content.write"]},{"oauth2":["content.write"]}],"parameters":[{"$ref":"#/components/parameters/XHCioSite"},{"$ref":"#/components/parameters/SiteQuery"},{"name":"categoryId","in":"path","required":true,"description":"The category's id. The route does not constrain the format; it is looked up among this help center's live categories, and anything else (another help center's category included) answers 404.","schema":{"type":"string"}},{"name":"on_orphan","in":"query","required":false,"description":"What happens to the category's articles and sub-categories.","schema":{"type":["string","null"],"enum":["refuse","uncategorize","reparent",null],"default":"refuse"}}],"responses":{"200":{"description":"Deleted (moved to the trash).","content":{"application/json":{"schema":{"type":"object","required":["status","message","on_orphan","articles_moved","children_moved","trashed_articles_refiled","trashed_children_refiled","moved_to"],"properties":{"status":{"const":"success"},"message":{"const":"Category deleted."},"on_orphan":{"enum":["refuse","uncategorize","reparent"],"description":"The policy applied."},"articles_moved":{"type":"integer","minimum":0,"description":"Articles re-filed out of the category, trashed ones included."},"children_moved":{"type":"integer","minimum":0,"description":"Sub-categories re-parented, trashed ones included."},"trashed_articles_refiled":{"type":"integer","minimum":0,"description":"How many of `articles_moved` are in the trash (invisible to this API)."},"trashed_children_refiled":{"type":"integer","minimum":0,"description":"How many of `children_moved` are in the trash."},"moved_to":{"type":"integer","minimum":0,"description":"The category content moved to under `reparent`; 0 when it went to Uncategorized / top level."}}}}}},"400":{"$ref":"#/components/responses/SiteRequired"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanUpgradeRequired"},"403":{"description":"Refused. Codes: `insufficient_scope`, `role_forbidden` (OAuth), `site_forbidden` (OAuth), `capability_forbidden` (OAuth only: the role lacks `categories.manage`, checked first, or `articles.publish`), or no code with the message \"You are not allowed to delete categories.\" from the form request.","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/InsufficientScopeError"},{"$ref":"#/components/schemas/RoleForbiddenError"},{"$ref":"#/components/schemas/SiteForbiddenError"},{"$ref":"#/components/schemas/Error"},{"allOf":[{"$ref":"#/components/schemas/CapabilityForbiddenError"},{"type":"object","properties":{"capability":{"enum":["categories.manage","articles.publish"]}}}]}]}}}},"404":{"$ref":"#/components/responses/NotFound","description":"`Category not found.` for a missing, trashed or other help center's category (the same body either way), or the help center behind this API key has been deleted."},"409":{"description":"`on_orphan` was `refuse` (or omitted) and the category still holds live content. Nothing was changed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CategoriesNotEmptyError"}}}},"422":{"$ref":"#/components/responses/ValidationFailed"},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/v1/categories/{categoryId}/articles/order":{"put":{"operationId":"reorderCategoryArticles","summary":"Set the reading order of one category's articles","description":"Writes each listed article's position in this category: `articles.pos` for an article filed only here, and also its `article_category` row for a multi-category article. Listed ids that are not members of this category (including other help centers' articles) are skipped and reported; unlisted members keep their positions. Article `updated_at` is not touched. The order is only displayed when the help center's category article sort is \"custom\"; the response says whether it is. Needs `categories.manage` and `articles.publish` on the OAuth lane. Write-limited.","tags":["Categories"],"security":[{"apiKey":["content.write"]},{"oauth2":["content.write"]}],"parameters":[{"$ref":"#/components/parameters/XHCioSite"},{"$ref":"#/components/parameters/SiteQuery"},{"name":"categoryId","in":"path","required":true,"description":"The category's id. The route does not constrain the format; it is looked up among this help center's live categories, and anything else (another help center's category included) answers 404.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["articles"],"properties":{"articles":{"type":"array","minItems":1,"maxItems":1000,"description":"The order to apply. If an id appears twice, its last entry wins.","items":{"type":"object","required":["id"],"properties":{"id":{"type":"integer","minimum":1,"description":"An article id. Not validated against the help center; non-members are reported in `skipped_article_ids`."},"position":{"type":["integer","null"],"minimum":0,"maximum":2147483647,"description":"Omitted or null: the item's 0-based index in `articles`."}}}}}}}}},"responses":{"200":{"description":"Positions written for every member listed.","content":{"application/json":{"schema":{"type":"object","required":["status","message","updated_count","skipped_article_ids","order_visible","article_sort","article_sort_source","hint"],"properties":{"status":{"const":"success"},"message":{"enum":["Article order saved.","Article order saved, but this help center does not currently display it."]},"updated_count":{"type":"integer","minimum":0,"description":"Articles whose position was written."},"skipped_article_ids":{"type":"array","items":{"type":"integer"},"description":"Requested ids that are not live members of this category; nothing was written for them."},"order_visible":{"type":"boolean","description":"Whether the help center currently renders category articles in this custom order (its sort is `custom`)."},"article_sort":{"type":"string","description":"The sort the help center's category pages use. Template v2: one of `title`, `updated_at`, `created_at`, `custom`. Template v1: the stored `articlesOrder` value, `modified` when unset."},"article_sort_source":{"enum":["template_v2.pages.category.articleListSort","template_v1.articlesOrder"],"description":"Where `article_sort` was read."},"hint":{"type":["string","null"],"description":"When `order_visible` is false, where in the customizer to switch the sort to Custom; otherwise null. The endpoint never changes the sort itself."}}}}}},"400":{"$ref":"#/components/responses/SiteRequired"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanUpgradeRequired"},"403":{"description":"Refused. Codes: `insufficient_scope`, `role_forbidden` (OAuth), `site_forbidden` (OAuth), `capability_forbidden` (OAuth only: the role lacks `categories.manage`, checked first, or `articles.publish`), or no code with the message \"You are not allowed to reorder articles.\" from the form request.","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/InsufficientScopeError"},{"$ref":"#/components/schemas/RoleForbiddenError"},{"$ref":"#/components/schemas/SiteForbiddenError"},{"$ref":"#/components/schemas/Error"},{"allOf":[{"$ref":"#/components/schemas/CapabilityForbiddenError"},{"type":"object","properties":{"capability":{"enum":["categories.manage","articles.publish"]}}}]}]}}}},"404":{"$ref":"#/components/responses/NotFound","description":"`Category not found.` for a missing, trashed or other help center's category (the same body either way), or the help center behind this API key has been deleted."},"422":{"$ref":"#/components/responses/ValidationFailed"},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/v1/articles/{articleId}/staged":{"get":{"operationId":"getArticleStagedChanges","summary":"Get an article's staged changes","description":"Returns the staged (not yet visible) copy of a published article. With `locale`, `staged` is that one locale's copy; without it, an array of every staged locale ordered by locale code. Answers 404 when nothing is staged, so it doubles as a probe. Counts against the write rate limit (`public-api-write`, 120 requests/minute per credential by default) as well as the read limit, although it only reads.","tags":["Staged changes"],"security":[{"apiKey":["content.read"]},{"oauth2":["content.read"]}],"parameters":[{"name":"articleId","in":"path","required":true,"description":"The article's id. Looked up only among the resolved help center's articles; another help center's id, or a private article the caller's role may not read, answers 404 exactly like a missing one.","schema":{"type":"integer"}},{"name":"locale","in":"query","required":false,"description":"Return only this locale's staged copy. Not validated: an unknown code simply finds nothing (404).","schema":{"type":"string"}},{"$ref":"#/components/parameters/XHCioSite"},{"$ref":"#/components/parameters/SiteQuery"}],"responses":{"200":{"description":"The staged copy or copies.","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"const":"success"},"staged":{"oneOf":[{"$ref":"#/components/schemas/StagedChange"},{"type":"array","items":{"$ref":"#/components/schemas/StagedChange"},"minItems":1}],"description":"One object when `locale` was given, otherwise an array."}},"required":["status","staged"]}}}},"400":{"$ref":"#/components/responses/SiteRequired"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanUpgradeRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"}}},"patch":{"operationId":"updateArticleStagedChanges","summary":"Create or update an article's staged changes","description":"Stages edits to one locale of a published article without readers seeing them; the live article is untouched until publish. Partial: fields you omit keep what was staged before, and article-wide fields (`category_id`, `categories`, `visibility`, `type`, `author_id`, `shared_with`, `metadata`) merge onto what the article already stages and are copied to every staged locale. If the result equals the live article, nothing is staged (any staged copy for the locale is removed) and `staged` is `null`. A title-only change never rewrites the slug unless the locale has none. Counts against the write rate limit (`public-api-write`, 120 requests/minute per credential by default) as well as the read limit. Takes optional `change_note` and `change_refs`: what you say about this staged save, kept with its activity record and never public. Saves by the same caller that join one editing session keep the newest note and the union of the references (5 at most, the oldest dropped).","tags":["Staged changes"],"security":[{"apiKey":["content.write"]},{"oauth2":["content.write"]}],"parameters":[{"name":"articleId","in":"path","required":true,"description":"The article's id. Looked up only among the resolved help center's articles; another help center's id, or a private article the caller's role may not read, answers 404 exactly like a missing one.","schema":{"type":"integer"}},{"$ref":"#/components/parameters/XHCioSite"},{"$ref":"#/components/parameters/SiteQuery"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"locale":{"allOf":[{"$ref":"#/components/schemas/StagedChangesLocaleCode"}],"description":"The language to stage. Staged changes are held per article and language."},"title":{"type":["string","null"],"description":"`null` or an empty string stages the title as cleared."},"content":{"type":["string","null"],"description":"HTML. Sanitised on the way in. `null` stages empty content."},"slug":{"type":["string","null"],"maxLength":190,"description":"Sending it (even empty) regenerates the slug: transliterated and de-duplicated, from the title when empty. An empty slug also clears `has_custom_slug`."},"audio_file":{"type":["string","null"],"description":"`null` stages removal of this language's audio file."},"has_custom_slug":{"type":["boolean","null"],"description":"Marks the slug hand-written. `null` is stored as `false`."},"category_id":{"type":["integer","null"],"description":"A category of this help center, or `0` for none."},"categories":{"type":["array","null"],"items":{"type":"integer"},"description":"Category ids of this help center (multi-category placement)."},"visibility":{"type":["string","null"],"enum":["public","private","link_only",null]},"type":{"type":["string","null"],"enum":["article","faq",null],"description":"`null` is stored as `article`."},"author_id":{"type":["integer","null"],"description":"A staff member of this help center, the article's current author, or an author this article already stages."},"shared_with":{"type":["array","null"],"items":{"type":"integer"},"description":"Private-access user ids: members of this help center, users the article is already shared with, or ids it already stages. `null` stages an empty list."},"metadata":{"type":["object","null"],"description":"SEO metadata, merged per language onto what is staged (or live). A `null` or empty value clears that language. Keys must be languages enabled on this help center.","properties":{"title":{"type":["object","null"],"additionalProperties":{"type":["string","null"],"maxLength":60}},"description":{"type":["object","null"],"additionalProperties":{"type":["string","null"],"maxLength":160}}},"additionalProperties":false},"release_translation":{"type":["boolean","null"],"description":"`true` marks this locale released when the staged changes publish; `false`/`null` unmarks it. Needs publishing permission."},"change_set_id":{"type":["integer","null"],"description":"Not supported: any non-zero value answers 422 `CHANGE_SETS_UNAVAILABLE`. Use `POST /v1/change-sets/{changeSetId}/items` instead."},"etag":{"type":["string","null"],"maxLength":64,"description":"The `etag` last read for this locale. A mismatch answers 409 `STALE_ETAG` (scope `locale`), including when nothing is staged for the locale yet. Omit to write unconditionally."},"shared_etag":{"type":["string","null"],"maxLength":64,"description":"The `shared_etag` last read, or the literal `none` if the read showed none. Checked only when this request would change the article-wide block; a mismatch answers 409 `STALE_ETAG` (scope `shared`). Omit to opt out (last write wins)."}},"required":["locale"],"allOf":[{"$ref":"#/components/schemas/ChangeNoteFields"},{"$ref":"#/components/schemas/ArticleWriteNoteFields"}]}}}},"responses":{"200":{"description":"The staged copy, or `staged: null` with a message when the values match the live article and nothing is staged.","content":{"application/json":{"schema":{"oneOf":[{"type":"object","properties":{"status":{"const":"success"},"staged":{"$ref":"#/components/schemas/StagedChange"},"notes":{"$ref":"#/components/schemas/CreatedArticleNotes"},"annotate_changes":{"$ref":"#/components/schemas/ArticleChangeAnnotations"}},"required":["status","staged"]},{"type":"object","properties":{"status":{"const":"success"},"staged":{"type":"null"},"message":{"type":"string"},"notes":{"$ref":"#/components/schemas/CreatedArticleNotes"},"annotate_changes":{"$ref":"#/components/schemas/ArticleChangeAnnotations"}},"required":["status","staged","message"]}]}}}},"400":{"$ref":"#/components/responses/SiteRequired"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanUpgradeRequired"},"403":{"description":"Codes: `insufficient_scope`, `role_forbidden`, `site_forbidden`; `capability_forbidden` (OAuth lane only) for `articles.edit` (always, and for `metadata`/`type`/`author_id`), `categories.manage` (`category_id`/`categories`), `content.share` (`visibility`/`shared_with`) or `articles.publish` (`release_translation`); `STAGING_UNAVAILABLE` when the plan does not include staged changes; or the request's own refusal without a code (\"You are not allowed to stage changes to articles with this API key\"). With `notes` or `annotate_changes`: also `insufficient_scope` / `role_forbidden` for `notes.write`, or `notes_forbidden`, exactly as the notes routes answer them, before anything is written.","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/InsufficientScopeError"},{"$ref":"#/components/schemas/RoleForbiddenError"},{"$ref":"#/components/schemas/SiteForbiddenError"},{"$ref":"#/components/schemas/Error"},{"allOf":[{"$ref":"#/components/schemas/CapabilityForbiddenError"}],"properties":{"capability":{"enum":["articles.edit","categories.manage","content.share","articles.publish"]}}},{"$ref":"#/components/schemas/StagedChangesPlanUnavailableError"},{"$ref":"#/components/schemas/NotesForbiddenError"}]}}}},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"`STALE_ETAG`: `etag` or `shared_etag` no longer matches.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StagedChangesStaleEtagError"}}}},"422":{"description":"Validation failed (`status: validation_error`; e.g. unsupported `locale`, a category or user outside this help center, `type` other than `article`/`faq`); `NOT_PUBLISHED`, `ARTICLE_UNPUBLISHED` or `CHANGE_SETS_UNAVAILABLE`; or a staging refusal without a code, e.g. staged locales that disagree on an article-wide field the request does not name. Or a note that cannot be placed (`note_quote_not_found`, `note_quote_ambiguous`, `note_lang_required`, `note_locale_not_written`, `note_quotes_overlap`, `annotate_changes_covered`): nothing was written.","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/ValidationError"},{"$ref":"#/components/schemas/StagedChangesNotStageableError"},{"$ref":"#/components/schemas/Error"},{"$ref":"#/components/schemas/NoteRefusedError"}]},"examples":{"referenceNotHttps":{"summary":"A reference that is not an https address","value":{"status":"validation_error","message":"The request could not be accepted.","errors":{"change_refs.0.url":["The change refs.0.url must start with https://."]}}},"referenceWithCredentials":{"summary":"A reference with a user name in the address","value":{"status":"validation_error","message":"The request could not be accepted.","errors":{"change_refs.1.url":["The change refs.1.url must not contain a user name or password."]}}},"noteTooLong":{"summary":"A note longer than 2,000 characters","value":{"status":"validation_error","message":"The request could not be accepted.","errors":{"change_note":["The change note may not be longer than 2,000 characters."]}}},"tooManyReferences":{"summary":"More than 5 references","value":{"status":"validation_error","message":"The request could not be accepted.","errors":{"change_refs":["A change can carry at most 5 references."]}}},"referenceShape":{"summary":"A reference with a key other than label and url","value":{"status":"validation_error","message":"The request could not be accepted.","errors":{"change_refs.0":["Each reference must be an object with only a label and a url."]}}}}}}},"429":{"$ref":"#/components/responses/TooManyRequests"}}},"delete":{"operationId":"discardArticleStagedChanges","summary":"Discard an article's staged changes","description":"Deletes the staged copy for one locale, or for every locale when `locale` is omitted. The live article is never touched. A locale-scoped discard keeps the article-wide block on the locales still staged. Counts against the write rate limit (`public-api-write`, 120 requests/minute per credential by default) as well as the read limit.","tags":["Staged changes"],"security":[{"apiKey":["content.write"]},{"oauth2":["content.write"]}],"parameters":[{"name":"articleId","in":"path","required":true,"description":"The article's id. Looked up only among the resolved help center's articles; another help center's id, or a private article the caller's role may not read, answers 404 exactly like a missing one.","schema":{"type":"integer"}},{"name":"locale","in":"query","required":false,"description":"Discard only this locale. Not validated: an unknown code discards nothing (404).","schema":{"type":"string"}},{"$ref":"#/components/parameters/XHCioSite"},{"$ref":"#/components/parameters/SiteQuery"}],"responses":{"200":{"description":"Discarded; the live article as it now stands.","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"const":"success"},"discarded":{"type":"integer","minimum":1,"description":"Number of staged rows deleted (may exceed one locale when a sibling left identical to live is removed too)."},"article":{"$ref":"#/components/schemas/Article"}},"required":["status","discarded","article"]}}}},"400":{"$ref":"#/components/responses/SiteRequired"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanUpgradeRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/v1/articles/{articleId}/staged/publish":{"post":{"operationId":"publishArticleStagedChanges","summary":"Publish an article's staged changes","description":"Applies staged changes to the live article: archives the outgoing live values (see versions), applies the fields and the article-wide block, and deletes the published staged copies. Omit `locale` to publish every staged locale. Requires publishing permission even when someone else staged the work. Counts against the write rate limit (`public-api-write`, 120 requests/minute per credential by default) as well as the read limit. Takes optional `change_note` and `change_refs`: what you say about this publish, kept with the activity records it makes and never public.","tags":["Staged changes"],"security":[{"apiKey":["content.write"]},{"oauth2":["content.write"]}],"parameters":[{"name":"articleId","in":"path","required":true,"description":"The article's id. Looked up only among the resolved help center's articles; another help center's id, or a private article the caller's role may not read, answers 404 exactly like a missing one.","schema":{"type":"integer"}},{"$ref":"#/components/parameters/XHCioSite"},{"$ref":"#/components/parameters/SiteQuery"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"locale":{"anyOf":[{"$ref":"#/components/schemas/StagedChangesLocaleCode"},{"type":"null"}],"description":"Publish only this locale. Omit or `null` to publish every staged locale."},"force":{"type":["boolean","null"],"description":"Publish even though the live article changed after the edits were staged, overwriting it."}},"allOf":[{"$ref":"#/components/schemas/ChangeNoteFields"}]}}}},"responses":{"200":{"description":"Published; the live article after the publish.","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"const":"success"},"published_locales":{"type":"array","items":{"type":"string"}},"article":{"$ref":"#/components/schemas/Article"}},"required":["status","published_locales","article"]}}}},"400":{"$ref":"#/components/responses/SiteRequired"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanUpgradeRequired"},"403":{"description":"Codes: `insufficient_scope`, `role_forbidden`, `site_forbidden`; `capability_forbidden` (OAuth lane only, capability `articles.publish`); or the request's own refusal without a code (\"You are not allowed to publish staged changes with this API key\").","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/InsufficientScopeError"},{"$ref":"#/components/schemas/RoleForbiddenError"},{"$ref":"#/components/schemas/SiteForbiddenError"},{"$ref":"#/components/schemas/Error"},{"allOf":[{"$ref":"#/components/schemas/CapabilityForbiddenError"}],"properties":{"capability":{"enum":["articles.publish"]}}}]}}}},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"`BASE_CHANGED`: the live article moved under a staged copy being published. Retry with `force: true` to overwrite.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StagedChangesBaseChangedError"}}}},"422":{"description":"Validation failed (`status: validation_error`), or a refusal without a code: nothing staged (for that locale), staged locales disagree on an article-wide field, the result would leave the article with no title in any language, a staged author is no longer on the team, or a staged category is not on this help center.","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/ValidationError"},{"$ref":"#/components/schemas/Error"}]},"examples":{"referenceNotHttps":{"summary":"A reference that is not an https address","value":{"status":"validation_error","message":"The request could not be accepted.","errors":{"change_refs.0.url":["The change refs.0.url must start with https://."]}}},"referenceWithCredentials":{"summary":"A reference with a user name in the address","value":{"status":"validation_error","message":"The request could not be accepted.","errors":{"change_refs.1.url":["The change refs.1.url must not contain a user name or password."]}}},"noteTooLong":{"summary":"A note longer than 2,000 characters","value":{"status":"validation_error","message":"The request could not be accepted.","errors":{"change_note":["The change note may not be longer than 2,000 characters."]}}},"tooManyReferences":{"summary":"More than 5 references","value":{"status":"validation_error","message":"The request could not be accepted.","errors":{"change_refs":["A change can carry at most 5 references."]}}},"referenceShape":{"summary":"A reference with a key other than label and url","value":{"status":"validation_error","message":"The request could not be accepted.","errors":{"change_refs.0":["Each reference must be an object with only a label and a url."]}}}}}}},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/v1/articles/{articleId}/versions":{"get":{"operationId":"listArticleVersions","summary":"List an article's archived versions","description":"The publication history: snapshots of what was live before each staged publish or restore, newest first. Up to 20 are kept per article and locale. Counts against the write rate limit (`public-api-write`, 120 requests/minute per credential by default) as well as the read limit, although it only reads.","tags":["Staged changes"],"security":[{"apiKey":["content.read"]},{"oauth2":["content.read"]}],"parameters":[{"name":"articleId","in":"path","required":true,"description":"The article's id. Looked up only among the resolved help center's articles; another help center's id, or a private article the caller's role may not read, answers 404 exactly like a missing one.","schema":{"type":"integer"}},{"name":"locale","in":"query","required":false,"description":"Only this locale's snapshots. Not validated.","schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"description":"Maximum snapshots returned; values outside 1–100 are clamped.","schema":{"type":"integer","minimum":1,"maximum":100,"default":20}},{"$ref":"#/components/parameters/XHCioSite"},{"$ref":"#/components/parameters/SiteQuery"}],"responses":{"200":{"description":"Archived versions.","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"const":"success"},"versions":{"type":"array","items":{"$ref":"#/components/schemas/StagedChangesArchivedVersion"},"description":"Ordered by `published_at` descending, then id descending."}},"required":["status","versions"]}}}},"400":{"$ref":"#/components/responses/SiteRequired"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanUpgradeRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/v1/articles/{articleId}/versions/{versionId}/restore":{"post":{"operationId":"restoreArticleVersion","summary":"Restore an archived version","description":"Rolls the live article (that version's locale) back to an archived snapshot. The outgoing live values are archived first, so a restore can itself be undone. Needs both restore and publishing permission. Counts against the write rate limit (`public-api-write`, 120 requests/minute per credential by default) as well as the read limit.","tags":["Staged changes"],"security":[{"apiKey":["content.write"]},{"oauth2":["content.write"]}],"parameters":[{"name":"articleId","in":"path","required":true,"description":"The article's id. Looked up only among the resolved help center's articles; another help center's id, or a private article the caller's role may not read, answers 404 exactly like a missing one.","schema":{"type":"integer"}},{"name":"versionId","in":"path","required":true,"description":"An archived version of this article on this help center (an id from `GET /v1/articles/{articleId}/versions`).","schema":{"type":"integer"}},{"$ref":"#/components/parameters/XHCioSite"},{"$ref":"#/components/parameters/SiteQuery"}],"responses":{"200":{"description":"Restored; the live article afterwards.","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"const":"success"},"article":{"$ref":"#/components/schemas/Article"}},"required":["status","article"]}}}},"400":{"$ref":"#/components/responses/SiteRequired"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanUpgradeRequired"},"403":{"description":"Codes: `insufficient_scope`, `role_forbidden`, `site_forbidden`; `capability_forbidden` (OAuth lane only) for `articles.restore` or `articles.publish`; or a refusal without a code (\"You are not allowed to modify staged changes with this credential.\").","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/InsufficientScopeError"},{"$ref":"#/components/schemas/RoleForbiddenError"},{"$ref":"#/components/schemas/SiteForbiddenError"},{"$ref":"#/components/schemas/Error"},{"allOf":[{"$ref":"#/components/schemas/CapabilityForbiddenError"}],"properties":{"capability":{"enum":["articles.restore","articles.publish"]}}}]}}}},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/v1/change-sets":{"get":{"operationId":"listChangeSets","summary":"List change sets","description":"The help center's change sets, most recently updated first, without their items. Counts against the write rate limit (`public-api-write`, 120 requests/minute per credential by default) as well as the read limit, although it only reads.","tags":["Change sets"],"security":[{"apiKey":["content.read"]},{"oauth2":["content.read"]}],"parameters":[{"name":"status","in":"query","required":false,"description":"Only sets with exactly this status (`open`, `publishing`, `published`, `partially_published`, `failed`, `cancelled`). Not validated: any other value returns an empty list.","schema":{"type":"string"}},{"name":"per_page","in":"query","required":false,"description":"Page size; values above 100 are clamped to 100.","schema":{"type":"integer","maximum":100,"default":25}},{"name":"page","in":"query","required":false,"description":"Page number (Laravel page-based pagination).","schema":{"type":"integer","minimum":1,"default":1}},{"$ref":"#/components/parameters/XHCioSite"},{"$ref":"#/components/parameters/SiteQuery"}],"responses":{"200":{"description":"A page of change sets.","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"const":"success"},"change_sets":{"type":"array","items":{"$ref":"#/components/schemas/ChangeSetSummary"}},"meta":{"type":"object","properties":{"current_page":{"type":"integer"},"last_page":{"type":"integer"},"total":{"type":"integer"}},"required":["current_page","last_page","total"]}},"required":["status","change_sets","meta"]}}}},"400":{"$ref":"#/components/responses/SiteRequired"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanUpgradeRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"}}},"post":{"operationId":"createChangeSet","summary":"Create a change set","description":"Opens an empty change set (a release folder that points at staged work). A help center may hold at most 25 open sets. Counts against the write rate limit (`public-api-write`, 120 requests/minute per credential by default) as well as the read limit.","tags":["Change sets"],"security":[{"apiKey":["content.write"]},{"oauth2":["content.write"]}],"parameters":[{"$ref":"#/components/parameters/XHCioSite"},{"$ref":"#/components/parameters/SiteQuery"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","maxLength":120,"description":"Must be non-empty when sent. Trimmed."},"description":{"type":["string","null"],"maxLength":2000}},"required":["name"]}}}},"responses":{"201":{"description":"Created.","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"const":"success"},"change_set":{"$ref":"#/components/schemas/ChangeSet"}},"required":["status","change_set"]}}}},"400":{"$ref":"#/components/responses/SiteRequired"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"description":"Either the shared plan refusal (MCP traffic to a help center whose plan does not include the MCP server), or `reason: CHANGE_SETS_UNAVAILABLE` from this endpoint: the help center's plan (or rollout) does not include change sets.","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/PlanUpgradeRequiredError"},{"$ref":"#/components/schemas/ChangeSetPlanUnavailableError"}]}}}},"403":{"description":"Codes: `insufficient_scope`, `role_forbidden`, `site_forbidden`; `capability_forbidden` (OAuth lane only) for `change_sets.manage`; or a refusal without a code: \"You are not allowed to manage change sets with this API key\" from the request, or \"You are not allowed to modify change sets with this credential.\"","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/InsufficientScopeError"},{"$ref":"#/components/schemas/RoleForbiddenError"},{"$ref":"#/components/schemas/SiteForbiddenError"},{"$ref":"#/components/schemas/Error"},{"allOf":[{"$ref":"#/components/schemas/CapabilityForbiddenError"}],"properties":{"capability":{"enum":["change_sets.manage"]}}}]}}}},"409":{"description":"`TOO_MANY_OPEN_CHANGE_SETS`: the help center already has 25 open change sets.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/ChangeSetConflictError"}],"properties":{"reason":{"enum":["TOO_MANY_OPEN_CHANGE_SETS"]}}}}}},"422":{"description":"Validation failed (`status: validation_error`), or `name` was omitted (`{\"status\":\"error\",\"message\":\"A change set needs a name.\"}`).","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/ValidationError"},{"$ref":"#/components/schemas/Error"}]}}}},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/v1/change-sets/{changeSetId}":{"get":{"operationId":"getChangeSet","summary":"Get a change set","description":"One change set with its items and readiness (`readiness.blockers` stop a publish; `readiness.warnings` never do). Counts against the write rate limit (`public-api-write`, 120 requests/minute per credential by default) as well as the read limit, although it only reads.","tags":["Change sets"],"security":[{"apiKey":["content.read"]},{"oauth2":["content.read"]}],"parameters":[{"name":"changeSetId","in":"path","required":true,"description":"The change set's id. Looked up only among the resolved help center's change sets; another help center's id answers 404.","schema":{"type":"integer"}},{"$ref":"#/components/parameters/XHCioSite"},{"$ref":"#/components/parameters/SiteQuery"}],"responses":{"200":{"description":"The change set.","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"const":"success"},"change_set":{"$ref":"#/components/schemas/ChangeSet"}},"required":["status","change_set"]}}}},"400":{"$ref":"#/components/responses/SiteRequired"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanUpgradeRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"}}},"patch":{"operationId":"updateChangeSet","summary":"Rename or re-describe a change set","description":"Allowed in any status, including after publishing. Counts against the write rate limit (`public-api-write`, 120 requests/minute per credential by default) as well as the read limit.","tags":["Change sets"],"security":[{"apiKey":["content.write"]},{"oauth2":["content.write"]}],"parameters":[{"name":"changeSetId","in":"path","required":true,"description":"The change set's id. Looked up only among the resolved help center's change sets; another help center's id answers 404.","schema":{"type":"integer"}},{"$ref":"#/components/parameters/XHCioSite"},{"$ref":"#/components/parameters/SiteQuery"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","maxLength":120,"description":"Must be non-empty when sent. Trimmed."},"description":{"type":["string","null"],"maxLength":2000}}}}}},"responses":{"200":{"description":"The updated change set.","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"const":"success"},"change_set":{"$ref":"#/components/schemas/ChangeSet"}},"required":["status","change_set"]}}}},"400":{"$ref":"#/components/responses/SiteRequired"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanUpgradeRequired"},"403":{"description":"Codes: `insufficient_scope`, `role_forbidden`, `site_forbidden`; `capability_forbidden` (OAuth lane only) for `change_sets.manage`; or a refusal without a code: \"You are not allowed to manage change sets with this API key\" from the request, or \"You are not allowed to modify change sets with this credential.\"","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/InsufficientScopeError"},{"$ref":"#/components/schemas/RoleForbiddenError"},{"$ref":"#/components/schemas/SiteForbiddenError"},{"$ref":"#/components/schemas/Error"},{"allOf":[{"$ref":"#/components/schemas/CapabilityForbiddenError"}],"properties":{"capability":{"enum":["change_sets.manage"]}}}]}}}},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/ValidationFailed"},"429":{"$ref":"#/components/responses/TooManyRequests"}}},"delete":{"operationId":"deleteChangeSet","summary":"Delete a change set","description":"Deletes the folder only: staged changes stay staged and unpublished articles stay unpublished. Refused while the set is publishing. Counts against the write rate limit (`public-api-write`, 120 requests/minute per credential by default) as well as the read limit.","tags":["Change sets"],"security":[{"apiKey":["content.write"]},{"oauth2":["content.write"]}],"parameters":[{"name":"changeSetId","in":"path","required":true,"description":"The change set's id. Looked up only among the resolved help center's change sets; another help center's id answers 404.","schema":{"type":"integer"}},{"$ref":"#/components/parameters/XHCioSite"},{"$ref":"#/components/parameters/SiteQuery"}],"responses":{"200":{"description":"Deleted.","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"const":"success"}},"required":["status"]}}}},"400":{"$ref":"#/components/responses/SiteRequired"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanUpgradeRequired"},"403":{"description":"Codes: `insufficient_scope`, `role_forbidden`, `site_forbidden`; `capability_forbidden` (OAuth lane only) for `change_sets.manage`; or a refusal without a code: \"You are not allowed to modify change sets with this credential.\"","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/InsufficientScopeError"},{"$ref":"#/components/schemas/RoleForbiddenError"},{"$ref":"#/components/schemas/SiteForbiddenError"},{"$ref":"#/components/schemas/Error"},{"allOf":[{"$ref":"#/components/schemas/CapabilityForbiddenError"}],"properties":{"capability":{"enum":["change_sets.manage"]}}}]}}}},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"`CHANGE_SET_NOT_OPEN`: the set is publishing.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/ChangeSetConflictError"}],"properties":{"reason":{"enum":["CHANGE_SET_NOT_OPEN"]}}}}}},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/v1/change-sets/{changeSetId}/items":{"post":{"operationId":"addChangeSetItems","summary":"Add articles and categories to a change set","description":"Batch add. A live article adds one item per staged locale; a never-published article adds an `article_publish` item; a category adds a visibility flip to `category_visibility`. Re-adding is idempotent. Answers 207 (`status: partial`) when some ids could not be added, listing them in `rejected`; the rest are added. A 402/409 refusal mid-batch leaves items already added in place. Counts against the write rate limit (`public-api-write`, 120 requests/minute per credential by default) as well as the read limit.","tags":["Change sets"],"security":[{"apiKey":["content.write"]},{"oauth2":["content.write"]}],"parameters":[{"name":"changeSetId","in":"path","required":true,"description":"The change set's id. Looked up only among the resolved help center's change sets; another help center's id answers 404.","schema":{"type":"integer"}},{"$ref":"#/components/parameters/XHCioSite"},{"$ref":"#/components/parameters/SiteQuery"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"article_ids":{"type":"array","maxItems":500,"items":{"type":"integer"}},"category_ids":{"type":"array","maxItems":500,"items":{"type":"integer"}},"category_visibility":{"type":"string","enum":["public","unlisted","private","team_private"],"default":"public","description":"The visibility the categories in `category_ids` get when the set publishes."}}}}}},"responses":{"200":{"description":"Everything was added.","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"const":"success"},"added":{"type":"integer","description":"Items added (one per staged locale for a live article)."},"rejected":{"type":"array","maxItems":0,"items":{"$ref":"#/components/schemas/ChangeSetItemRejection"}},"change_set":{"$ref":"#/components/schemas/ChangeSet"}},"required":["status","added","rejected","change_set"]}}}},"207":{"description":"Some ids could not be added; the others were.","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"const":"partial"},"added":{"type":"integer"},"rejected":{"type":"array","minItems":1,"items":{"$ref":"#/components/schemas/ChangeSetItemRejection"}},"change_set":{"$ref":"#/components/schemas/ChangeSet"}},"required":["status","added","rejected","change_set"]}}}},"400":{"$ref":"#/components/responses/SiteRequired"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"description":"Either the shared plan refusal (MCP traffic to a help center whose plan does not include the MCP server), or `reason: CHANGE_SETS_UNAVAILABLE` from this endpoint: the help center's plan (or rollout) does not include change sets.","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/PlanUpgradeRequiredError"},{"$ref":"#/components/schemas/ChangeSetPlanUnavailableError"}]}}}},"403":{"description":"Codes: `insufficient_scope`, `role_forbidden`, `site_forbidden`; `capability_forbidden` (OAuth lane only) for `change_sets.manage`; or a refusal without a code: \"You are not allowed to add items to a change set with this API key\" from the request, or \"You are not allowed to modify change sets with this credential.\"","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/InsufficientScopeError"},{"$ref":"#/components/schemas/RoleForbiddenError"},{"$ref":"#/components/schemas/SiteForbiddenError"},{"$ref":"#/components/schemas/Error"},{"allOf":[{"$ref":"#/components/schemas/CapabilityForbiddenError"}],"properties":{"capability":{"enum":["change_sets.manage"]}}}]}}}},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"`CHANGE_SET_NOT_OPEN`: the set is publishing or published. `CHANGE_SET_FULL`: it already holds 500 items.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/ChangeSetConflictError"}],"properties":{"reason":{"enum":["CHANGE_SET_NOT_OPEN","CHANGE_SET_FULL"]}}}}}},"422":{"$ref":"#/components/responses/ValidationFailed"},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/v1/change-sets/{changeSetId}/items/{itemId}":{"delete":{"operationId":"removeChangeSetItem","summary":"Remove an item from a change set","description":"Takes one item out of the release. The staged version, article or category it pointed at is untouched. Counts against the write rate limit (`public-api-write`, 120 requests/minute per credential by default) as well as the read limit.","tags":["Change sets"],"security":[{"apiKey":["content.write"]},{"oauth2":["content.write"]}],"parameters":[{"name":"changeSetId","in":"path","required":true,"description":"The change set's id. Looked up only among the resolved help center's change sets; another help center's id answers 404.","schema":{"type":"integer"}},{"name":"itemId","in":"path","required":true,"description":"An item of this change set (`change_set.items[].id`). An item of another set answers 404.","schema":{"type":"integer"}},{"$ref":"#/components/parameters/XHCioSite"},{"$ref":"#/components/parameters/SiteQuery"}],"responses":{"200":{"description":"Removed; the change set afterwards.","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"const":"success"},"change_set":{"$ref":"#/components/schemas/ChangeSet"}},"required":["status","change_set"]}}}},"400":{"$ref":"#/components/responses/SiteRequired"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanUpgradeRequired"},"403":{"description":"Codes: `insufficient_scope`, `role_forbidden`, `site_forbidden`; `capability_forbidden` (OAuth lane only) for `change_sets.manage`; or a refusal without a code: \"You are not allowed to modify change sets with this credential.\"","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/InsufficientScopeError"},{"$ref":"#/components/schemas/RoleForbiddenError"},{"$ref":"#/components/schemas/SiteForbiddenError"},{"$ref":"#/components/schemas/Error"},{"allOf":[{"$ref":"#/components/schemas/CapabilityForbiddenError"}],"properties":{"capability":{"enum":["change_sets.manage"]}}}]}}}},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"`CHANGE_SET_NOT_OPEN`: the set is publishing or published.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/ChangeSetConflictError"}],"properties":{"reason":{"enum":["CHANGE_SET_NOT_OPEN"]}}}}}},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/v1/change-sets/{changeSetId}/publish":{"post":{"operationId":"publishChangeSet","summary":"Publish or schedule a change set","description":"Always 202: publishing runs in a queued job, so poll `status_url` (`GET /v1/change-sets/{changeSetId}/status`) until `done`. With `scheduled_for`, arms the release instead (`status: scheduled`); only an `open` set can be scheduled. Refused with 422 and `readiness` when the set is empty or has blockers. A refusal writes nothing (release note and publisher unchanged). Also used to retry a `failed` or `partially_published` set. Counts against the write rate limit (`public-api-write`, 120 requests/minute per credential by default) as well as the read limit. Takes optional `change_note` and `change_refs`, unlike `release_note` (the set's own label) not shown with the change set and never public. Published now, they go on the activity record of every article the run changes. With `scheduled_for` they are kept with the schedule and used when it runs; arming the schedule again, or cancelling it, without them clears them. A refusal writes nothing.","tags":["Change sets"],"security":[{"apiKey":["content.write"]},{"oauth2":["content.write"]}],"parameters":[{"name":"changeSetId","in":"path","required":true,"description":"The change set's id. Looked up only among the resolved help center's change sets; another help center's id answers 404.","schema":{"type":"integer"}},{"$ref":"#/components/parameters/XHCioSite"},{"$ref":"#/components/parameters/SiteQuery"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"scheduled_for":{"type":["string","null"],"description":"When to publish; must be in the future. Any date PHP can parse; a UTC offset in an ISO 8601 time is honoured. Omit to publish now. Arming a set that is already scheduled replaces the schedule, and without `change_note` and `change_refs` in the new request clears the ones kept from the earlier arming: the note belongs to the arming that sent it. The release note is the set's label and stays until you send another."},"release_note":{"type":["string","null"],"maxLength":500}},"allOf":[{"$ref":"#/components/schemas/ChangeNoteFields"}]}}}},"responses":{"202":{"description":"Accepted: queued now (`status: queued`) or armed (`status: scheduled`).","content":{"application/json":{"schema":{"oneOf":[{"type":"object","properties":{"status":{"const":"queued"},"change_set":{"$ref":"#/components/schemas/ChangeSet"},"status_url":{"type":"string","format":"uri","description":"Absolute URL of `GET /v1/change-sets/{changeSetId}/status`."}},"required":["status","change_set","status_url"]},{"type":"object","properties":{"status":{"const":"scheduled"},"change_set":{"$ref":"#/components/schemas/ChangeSet"}},"required":["status","change_set"]}]}}}},"400":{"$ref":"#/components/responses/SiteRequired"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"description":"Either the shared plan refusal (MCP traffic to a help center whose plan does not include the MCP server), or `reason: CHANGE_SETS_UNAVAILABLE` from this endpoint: the help center's plan (or rollout) does not include change sets.","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/PlanUpgradeRequiredError"},{"$ref":"#/components/schemas/ChangeSetPlanUnavailableError"}]}}}},"403":{"description":"Codes: `insufficient_scope`, `role_forbidden`, `site_forbidden`; `capability_forbidden` (OAuth lane only) for `change_sets.manage` or `articles.publish`; or a refusal without a code: \"You are not allowed to publish a change set with this API key\" from the request, or \"You are not allowed to modify change sets with this credential.\"","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/InsufficientScopeError"},{"$ref":"#/components/schemas/RoleForbiddenError"},{"$ref":"#/components/schemas/SiteForbiddenError"},{"$ref":"#/components/schemas/Error"},{"allOf":[{"$ref":"#/components/schemas/CapabilityForbiddenError"}],"properties":{"capability":{"enum":["change_sets.manage","articles.publish"]}}}]}}}},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"`CHANGE_SET_NOT_OPEN`: the set is publishing or already published (including one reserved by a concurrent publish). `CHANGE_SET_NOT_SCHEDULABLE`: `scheduled_for` was sent for a `failed` or `partially_published` set; checked before readiness.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/ChangeSetConflictError"}],"properties":{"reason":{"enum":["CHANGE_SET_NOT_OPEN","CHANGE_SET_NOT_SCHEDULABLE"]}}}}}},"422":{"description":"Validation failed (`status: validation_error`; e.g. `scheduled_for` in the past); the set is not publishable (`readiness` included); or a refusal without a code, e.g. the publish job could not be queued (nothing was published and the set is back as it was).","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/ValidationError"},{"$ref":"#/components/schemas/ChangeSetReadinessError"},{"$ref":"#/components/schemas/Error"}]},"examples":{"referenceNotHttps":{"summary":"A reference that is not an https address","value":{"status":"validation_error","message":"The request could not be accepted.","errors":{"change_refs.0.url":["The change refs.0.url must start with https://."]}}},"referenceWithCredentials":{"summary":"A reference with a user name in the address","value":{"status":"validation_error","message":"The request could not be accepted.","errors":{"change_refs.1.url":["The change refs.1.url must not contain a user name or password."]}}},"noteTooLong":{"summary":"A note longer than 2,000 characters","value":{"status":"validation_error","message":"The request could not be accepted.","errors":{"change_note":["The change note may not be longer than 2,000 characters."]}}},"tooManyReferences":{"summary":"More than 5 references","value":{"status":"validation_error","message":"The request could not be accepted.","errors":{"change_refs":["A change can carry at most 5 references."]}}},"referenceShape":{"summary":"A reference with a key other than label and url","value":{"status":"validation_error","message":"The request could not be accepted.","errors":{"change_refs.0":["Each reference must be an object with only a label and a url."]}}}}}}},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/v1/change-sets/{changeSetId}/schedule":{"delete":{"operationId":"unscheduleChangeSet","summary":"Cancel a change set's schedule","description":"Clears `scheduled_for`, and with it the `change_note` and `change_refs` that were kept for the run (nothing is published, so the words said about that arming go with it; arm the schedule again with new ones if they still apply). Idempotent: a set with no schedule answers 200 too, and a date left on a `failed` or `partially_published` set can be cleared. Needs `change_sets.manage` but not `articles.publish`. Counts against the write rate limit (`public-api-write`, 120 requests/minute per credential by default) as well as the read limit.","tags":["Change sets"],"security":[{"apiKey":["content.write"]},{"oauth2":["content.write"]}],"parameters":[{"name":"changeSetId","in":"path","required":true,"description":"The change set's id. Looked up only among the resolved help center's change sets; another help center's id answers 404.","schema":{"type":"integer"}},{"$ref":"#/components/parameters/XHCioSite"},{"$ref":"#/components/parameters/SiteQuery"}],"responses":{"200":{"description":"The change set without a schedule.","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"const":"success"},"change_set":{"$ref":"#/components/schemas/ChangeSet"}},"required":["status","change_set"]}}}},"400":{"$ref":"#/components/responses/SiteRequired"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanUpgradeRequired"},"403":{"description":"Codes: `insufficient_scope`, `role_forbidden`, `site_forbidden`; `capability_forbidden` (OAuth lane only) for `change_sets.manage`; or a refusal without a code: \"You are not allowed to modify change sets with this credential.\"","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/InsufficientScopeError"},{"$ref":"#/components/schemas/RoleForbiddenError"},{"$ref":"#/components/schemas/SiteForbiddenError"},{"$ref":"#/components/schemas/Error"},{"allOf":[{"$ref":"#/components/schemas/CapabilityForbiddenError"}],"properties":{"capability":{"enum":["change_sets.manage"]}}}]}}}},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"`CHANGE_SET_NOT_OPEN`: the set is publishing, published or cancelled.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/ChangeSetConflictError"}],"properties":{"reason":{"enum":["CHANGE_SET_NOT_OPEN"]}}}}}},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/v1/change-sets/{changeSetId}/status":{"get":{"operationId":"getChangeSetStatus","summary":"Poll a change set's publish status","description":"The polling endpoint for a publish. Branch on `done`, not on the status string: it is false while the set is publishing or waiting for its schedule, and true otherwise (including after a run stopped early). Counts against the write rate limit (`public-api-write`, 120 requests/minute per credential by default) as well as the read limit, although it only reads.","tags":["Change sets"],"security":[{"apiKey":["content.read"]},{"oauth2":["content.read"]}],"parameters":[{"name":"changeSetId","in":"path","required":true,"description":"The change set's id. Looked up only among the resolved help center's change sets; another help center's id answers 404.","schema":{"type":"integer"}},{"$ref":"#/components/parameters/XHCioSite"},{"$ref":"#/components/parameters/SiteQuery"}],"responses":{"200":{"description":"Current status and per-item outcomes.","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"const":"success"},"change_set_status":{"$ref":"#/components/schemas/ChangeSetStatus"},"done":{"type":"boolean","description":"False while `publishing` or scheduled; true otherwise."},"processed_items":{"type":"integer"},"total_items":{"type":"integer"},"published_at":{"type":["string","null"],"format":"date-time"},"items":{"type":"array","items":{"$ref":"#/components/schemas/ChangeSetStatusItem"}}},"required":["status","change_set_status","done","processed_items","total_items","published_at","items"]}}}},"400":{"$ref":"#/components/responses/SiteRequired"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanUpgradeRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/v1/articles/{articleId}/comments":{"get":{"operationId":"listArticleComments","summary":"List an article's comments as threads","description":"One article's reader comments as threads: top-level comments with their replies nested under `replies` to any depth. Not paginated. The same validator as `GET /v1/comments` runs, so `article_id`, `search`, `page` and `limit` are validated but have no effect here. For a Viewer or Translator on the OAuth lane, an article the dashboard hides from them answers 404.","tags":["Comments"],"security":[{"apiKey":["content.read"]},{"oauth2":["content.read"]}],"parameters":[{"$ref":"#/components/parameters/XHCioSite"},{"$ref":"#/components/parameters/SiteQuery"},{"name":"articleId","in":"path","required":true,"description":"Id of an article on the resolved help center. Not route-constrained: an id that matches no article of this help center answers 404.","schema":{"type":"integer"}},{"name":"status","in":"query","required":false,"description":"Moderation state to return, applied at every depth of the thread (a pending reply under a published comment is dropped by `published`). `deleted` returns only soft-deleted comments at every depth.","schema":{"type":"string","enum":["pending","published","unpublished","deleted","all"],"default":"all"}},{"name":"lang","in":"query","required":false,"description":"Only comments in this language, at every depth of the thread. Must be the help center's default language or one of its added languages, otherwise 422.","schema":{"type":"string"}},{"name":"order","in":"query","required":false,"description":"Sort of the top-level comments by creation time.","schema":{"type":"string","enum":["asc","desc"],"default":"asc"}},{"name":"include_email","in":"query","required":false,"description":"`1` adds `author.email` to every comment in the response. Needs a write-capable credential (`content.write`) — a read-only one gets 403 — and, on the OAuth lane, the `comments.moderate` capability (403 `capability_forbidden`). Validated with Laravel's `boolean` rule, so a query string accepts only `1` or `0`: `true`/`false` answer 422.","schema":{"type":"integer","enum":[0,1]}},{"name":"article_id","in":"query","required":false,"description":"Validated as on `GET /v1/comments` (an invalid value answers 422), then ignored on this route.","schema":{"type":"integer","minimum":1}},{"name":"search","in":"query","required":false,"description":"Validated as on `GET /v1/comments` (an invalid value answers 422), then ignored on this route.","schema":{"type":"string","maxLength":190}},{"name":"page","in":"query","required":false,"description":"Validated as on `GET /v1/comments` (an invalid value answers 422), then ignored on this route.","schema":{"type":"integer","minimum":1,"default":1}},{"name":"limit","in":"query","required":false,"description":"Validated as on `GET /v1/comments` (an invalid value answers 422), then ignored on this route.","schema":{"type":"integer","minimum":1,"maximum":100,"default":25}}],"responses":{"200":{"description":"Success.","content":{"application/json":{"schema":{"type":"object","required":["status","article","comments","meta"],"properties":{"status":{"const":"success"},"article":{"type":"object","required":["id","title"],"properties":{"id":{"type":"integer"},"title":{"type":"object","additionalProperties":{"type":"string"},"description":"Every translation of the article title, keyed by language code."}}},"comments":{"type":"array","description":"Top-level comments; each carries `replies` (and so does every reply) but no `article` key.","items":{"allOf":[{"$ref":"#/components/schemas/Comment"},{"required":["replies"]}]}},"meta":{"type":"object","required":["threads_count","comments_count"],"properties":{"threads_count":{"type":"integer","minimum":0,"description":"Top-level comments returned."},"comments_count":{"type":"integer","minimum":0,"description":"Every comment in the payload, at every depth."}}}}}}}},"400":{"$ref":"#/components/responses/SiteRequired"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanUpgradeRequired"},"403":{"description":"Refused. Codes: `insufficient_scope` (credential lacks `content.read`), `role_forbidden`, `site_forbidden`, `capability_forbidden` with `capability: comments.moderate` (OAuth: `include_email` asked for by a person who may not moderate comments; message \"Your role on this help center does not allow reading commenter email addresses. Omit include_email to read the rest.\"), or no code with the message \"You are not allowed to read commenter email addresses with a read-scoped API key. Personal data needs a write-scoped key; omit include_email to read the rest.\" (`include_email` without `content.write`).","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/InsufficientScopeError"},{"$ref":"#/components/schemas/RoleForbiddenError"},{"$ref":"#/components/schemas/SiteForbiddenError"},{"$ref":"#/components/schemas/Error"},{"$ref":"#/components/schemas/CapabilityForbiddenError"}]}}}},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/ValidationFailed"},"429":{"$ref":"#/components/responses/TooManyRequests"}}},"post":{"operationId":"createArticleComment","summary":"Post a comment or reply","description":"Posts a comment on the article — a reply when `parent_id` is given — attributed to the user behind the credential, and publishes it immediately. The body is stored HTML-escaped, so markup renders as literal text on the help center. A top-level comment emails the article's author and a reply emails the parent comment's author (unless they unsubscribed or wrote the reply); the article's cached pages are purged. On the OAuth lane this needs the `comments.moderate` capability; also counts against the write rate limit (`public-api-write`, 120 requests/minute per credential by default).","tags":["Comments"],"security":[{"apiKey":["content.write"]},{"oauth2":["content.write"]}],"parameters":[{"$ref":"#/components/parameters/XHCioSite"},{"$ref":"#/components/parameters/SiteQuery"},{"name":"articleId","in":"path","required":true,"description":"Id of an article on the resolved help center. Not route-constrained: an id that matches no article of this help center answers 404.","schema":{"type":"integer"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["comment"],"properties":{"comment":{"type":"string","minLength":1,"maxLength":5000,"description":"Plain text. HTML is escaped on storage."},"lang":{"type":["string","null"],"description":"The help center's default language or one of its added languages (otherwise 422). Defaults to the parent comment's language, else the help center's default language."},"parent_id":{"type":["integer","null"],"minimum":1,"description":"Reply to this comment. Must be a non-deleted reader comment (any status) on this same article, otherwise 404."}}}}}},"responses":{"201":{"description":"The new comment (status `published`, never with `author.email`).","content":{"application/json":{"schema":{"type":"object","required":["status","message","comment"],"properties":{"status":{"const":"success"},"message":{"const":"Comment posted."},"comment":{"allOf":[{"$ref":"#/components/schemas/Comment"},{"required":["article"]}]}}}}}},"400":{"$ref":"#/components/responses/SiteRequired"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanUpgradeRequired"},"403":{"description":"Refused. Codes: `insufficient_scope` (credential lacks `content.write`), `role_forbidden` (OAuth: the person's role does not allow `content.write` here), `site_forbidden` (the selected help center is not part of the account), `capability_forbidden` with `capability: comments.moderate` (OAuth: the person may not moderate comments; message \"Your role on this help center does not allow replying to comments.\"), or no code with the message \"You are not allowed to post comments with this API key\".","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/InsufficientScopeError"},{"$ref":"#/components/schemas/RoleForbiddenError"},{"$ref":"#/components/schemas/SiteForbiddenError"},{"$ref":"#/components/schemas/Error"},{"$ref":"#/components/schemas/CapabilityForbiddenError"}]}}}},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/ValidationFailed"},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/v1/comments":{"get":{"operationId":"listComments","summary":"List comments","description":"Every reader comment on the help center as one flat, page-numbered list (replies are their own rows; no `replies` key), newest first by default. Internal editorial notes are never included. On the OAuth lane a Viewer or Translator only sees comments on articles the dashboard shows them, and `meta.status_counts` is narrowed the same way.","tags":["Comments"],"security":[{"apiKey":["content.read"]},{"oauth2":["content.read"]}],"parameters":[{"$ref":"#/components/parameters/XHCioSite"},{"$ref":"#/components/parameters/SiteQuery"},{"name":"status","in":"query","required":false,"description":"Moderation state to return. `deleted` returns only soft-deleted comments (otherwise excluded); `all` returns every non-deleted comment.","schema":{"type":"string","enum":["pending","published","unpublished","deleted","all"],"default":"all"}},{"name":"article_id","in":"query","required":false,"description":"Only comments on this article. Looked up among the resolved help center's articles; an id that is not one of them (or, for a Viewer or Translator on the OAuth lane, an article the dashboard hides from them) answers 404 rather than an empty list.","schema":{"type":"integer","minimum":1}},{"name":"lang","in":"query","required":false,"description":"Only comments written in this language. Must be the help center's default language or one of its added languages, otherwise 422.","schema":{"type":"string"}},{"name":"search","in":"query","required":false,"description":"Substring match (SQL `LIKE '%…%'`) against the stored comment body, which is HTML-escaped for visitor comments and HTML for staff ones. `%` and `_` are not escaped and act as wildcards.","schema":{"type":"string","maxLength":190}},{"name":"order","in":"query","required":false,"description":"Sort by creation time.","schema":{"type":"string","enum":["asc","desc"],"default":"desc"}},{"name":"page","in":"query","required":false,"description":"1-based page number. A page past the end returns an empty `comments` array.","schema":{"type":"integer","minimum":1,"default":1}},{"name":"limit","in":"query","required":false,"description":"Comments per page.","schema":{"type":"integer","minimum":1,"maximum":100,"default":25}},{"name":"include_email","in":"query","required":false,"description":"`1` adds `author.email` to every comment in the response. Needs a write-capable credential (`content.write`) — a read-only one gets 403 — and, on the OAuth lane, the `comments.moderate` capability (403 `capability_forbidden`). Validated with Laravel's `boolean` rule, so a query string accepts only `1` or `0`: `true`/`false` answer 422.","schema":{"type":"integer","enum":[0,1]}}],"responses":{"200":{"description":"Success.","content":{"application/json":{"schema":{"type":"object","required":["status","comments","meta"],"properties":{"status":{"const":"success"},"comments":{"type":"array","items":{"allOf":[{"$ref":"#/components/schemas/Comment"},{"required":["article"]}]}},"meta":{"type":"object","required":["page","per_page","total_pages","items_count","status_counts"],"properties":{"page":{"type":"integer","minimum":1,"description":"The requested page, echoed."},"per_page":{"type":"integer","minimum":1,"maximum":100,"description":"The effective `limit`."},"total_pages":{"type":"integer","minimum":1,"description":"ceil(items_count / per_page); 1 when nothing matches."},"items_count":{"type":"integer","minimum":0,"description":"Comments matching the filters, across all pages."},"status_counts":{"type":"object","description":"The help center's moderation queue, ignoring this request's filters. `pending`, `published` and `unpublished` count non-deleted comments; `deleted` counts soft-deleted ones.","required":["pending","published","unpublished","deleted"],"properties":{"pending":{"type":"integer","minimum":0},"published":{"type":"integer","minimum":0},"unpublished":{"type":"integer","minimum":0},"deleted":{"type":"integer","minimum":0}}}}}}}}}},"400":{"$ref":"#/components/responses/SiteRequired"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanUpgradeRequired"},"403":{"description":"Refused. Codes: `insufficient_scope` (credential lacks `content.read`), `role_forbidden`, `site_forbidden`, `capability_forbidden` with `capability: comments.moderate` (OAuth: `include_email` asked for by a person who may not moderate comments; message \"Your role on this help center does not allow reading commenter email addresses. Omit include_email to read the rest.\"), or no code with the message \"You are not allowed to read commenter email addresses with a read-scoped API key. Personal data needs a write-scoped key; omit include_email to read the rest.\" (`include_email` without `content.write`).","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/InsufficientScopeError"},{"$ref":"#/components/schemas/RoleForbiddenError"},{"$ref":"#/components/schemas/SiteForbiddenError"},{"$ref":"#/components/schemas/Error"},{"$ref":"#/components/schemas/CapabilityForbiddenError"}]}}}},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/ValidationFailed"},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/v1/comments/{commentId}":{"patch":{"operationId":"moderateComment","summary":"Approve or reject a comment","description":"Sets a reader comment's moderation status; its body cannot be edited. Approving a reply that was not already published emails the parent comment's author (at most once per reply), and any change in published state purges the article's cached pages. A soft-deleted comment answers 404 until it is restored in the dashboard. Also counts against the write rate limit (`public-api-write`, 120 requests/minute per credential by default).","tags":["Comments"],"security":[{"apiKey":["content.write"]},{"oauth2":["content.write"]}],"parameters":[{"$ref":"#/components/parameters/XHCioSite"},{"$ref":"#/components/parameters/SiteQuery"},{"name":"commentId","in":"path","required":true,"description":"Id of a reader comment on the resolved help center.","schema":{"type":"integer"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["status"],"properties":{"status":{"type":"string","enum":["published","unpublished"],"description":"`published` approves, `unpublished` rejects."}}}}}},"responses":{"200":{"description":"The moderated comment (never with `author.email`).","content":{"application/json":{"schema":{"type":"object","required":["status","message","comment"],"properties":{"status":{"const":"success"},"message":{"type":"string","enum":["Comment published.","Comment unpublished."]},"comment":{"allOf":[{"$ref":"#/components/schemas/Comment"},{"required":["article"]}]}}}}}},"400":{"$ref":"#/components/responses/SiteRequired"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanUpgradeRequired"},"403":{"description":"Refused. Codes: `insufficient_scope` (credential lacks `content.write`), `role_forbidden` (OAuth: the person's role does not allow `content.write` here), `site_forbidden` (the selected help center is not part of the account), `capability_forbidden` with `capability: comments.moderate` (OAuth: the person may not moderate comments; message \"Your role on this help center does not allow moderating comments.\"), or no code with the message \"You are not allowed to moderate comments with this API key\".","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/InsufficientScopeError"},{"$ref":"#/components/schemas/RoleForbiddenError"},{"$ref":"#/components/schemas/SiteForbiddenError"},{"$ref":"#/components/schemas/Error"},{"$ref":"#/components/schemas/CapabilityForbiddenError"}]}}}},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/ValidationFailed"},"429":{"$ref":"#/components/responses/TooManyRequests"}}},"delete":{"operationId":"deleteComment","summary":"Delete a comment","description":"Soft-deletes a reader comment: its replies stay, it can be restored from the dashboard, and the article's cached pages are purged. Idempotent — an already-deleted comment answers 200 with \"Comment was already deleted.\". Only an Owner or Admin of the help center may delete (403 `role_forbidden`, checked before the comment is looked up); an API key always qualifies, since only Owners and Admins can hold one. Also counts against the write rate limit (`public-api-write`, 120 requests/minute per credential by default).","tags":["Comments"],"security":[{"apiKey":["content.write"]},{"oauth2":["content.write"]}],"parameters":[{"$ref":"#/components/parameters/XHCioSite"},{"$ref":"#/components/parameters/SiteQuery"},{"name":"commentId","in":"path","required":true,"description":"Id of a reader comment on the resolved help center.","schema":{"type":"integer"}}],"responses":{"200":{"description":"Success.","content":{"application/json":{"schema":{"type":"object","required":["status","message","id"],"properties":{"status":{"const":"success"},"message":{"type":"string","enum":["Comment deleted. It can be restored from the dashboard.","Comment was already deleted."]},"id":{"type":"integer"}}}}}},"400":{"$ref":"#/components/responses/SiteRequired"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanUpgradeRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/v1/articles/{articleId}/notes":{"get":{"operationId":"listArticleNotes","summary":"List an article's editorial notes","description":"The team's internal editorial notes on an article (never reader comments) as threads with their replies, oldest first; resolved threads are left out unless `include_resolved=1`. Needs the separately granted `notes.read` scope, and the user behind the credential must still hold the notes permission on this help center. An article private to others, or in a private category the user is not a member of, answers 404 exactly like a missing one.","tags":["Notes"],"security":[{"apiKey":["notes.read"]},{"oauth2":["notes.read"]}],"parameters":[{"$ref":"#/components/parameters/XHCioSite"},{"$ref":"#/components/parameters/SiteQuery"},{"name":"articleId","in":"path","required":true,"description":"Id of an article on the resolved help center. Not route-constrained: an id that matches no article of this help center answers 404.","schema":{"type":"integer"}},{"name":"include_resolved","in":"query","required":false,"description":"`1` also returns resolved threads (marked `resolved: true`) and resolved replies. Validated with Laravel's `boolean` rule, so a query string accepts only `1` or `0`: `true`/`false` answer 422.","schema":{"type":"integer","enum":[0,1]}},{"name":"lang","in":"query","required":false,"description":"Only top-level notes in this exact language (not checked against the help center's languages). Replies are not filtered.","schema":{"type":"string","maxLength":12}}],"responses":{"200":{"description":"Success.","content":{"application/json":{"schema":{"type":"object","required":["status","article","notes","meta"],"properties":{"status":{"const":"success"},"article":{"type":"object","required":["id","title"],"properties":{"id":{"type":"integer"},"title":{"type":"object","additionalProperties":{"type":"string"},"description":"Every translation of the article title, keyed by language code."}}},"notes":{"type":"array","items":{"allOf":[{"$ref":"#/components/schemas/ArticleNote"},{"required":["replies"]}]}},"meta":{"type":"object","required":["threads_count","open_count","includes_resolved"],"properties":{"threads_count":{"type":"integer","minimum":0,"description":"Top-level notes returned."},"open_count":{"type":"integer","minimum":0,"description":"Returned top-level notes that are not resolved."},"includes_resolved":{"type":"boolean","description":"Echoes `include_resolved`."}}}}}}}},"400":{"$ref":"#/components/responses/SiteRequired"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanUpgradeRequired"},"403":{"description":"Refused. Codes: `insufficient_scope` (the credential was not granted the notes scope — content scopes do not imply it), `role_forbidden` (OAuth: the person's role on this help center does not allow it, e.g. a Viewer), `site_forbidden` (the selected help center is not part of the account), `notes_forbidden` (the user behind the credential no longer holds the `notes.read` permission on this help center, re-checked on every request), or no code with the message \"You are not allowed to read editorial notes with this credential. Notes need the notes.read scope, which is granted separately from content access.\".","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/InsufficientScopeError"},{"$ref":"#/components/schemas/RoleForbiddenError"},{"$ref":"#/components/schemas/SiteForbiddenError"},{"$ref":"#/components/schemas/Error"},{"$ref":"#/components/schemas/NotesForbiddenError"}]}}}},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/ValidationFailed"},"429":{"$ref":"#/components/responses/TooManyRequests"}}},"post":{"operationId":"createArticleNote","summary":"Start an editorial note on a passage","description":"Starts an internal note on a passage already in the article, attributed to the user behind the credential (an AI agent's key: \"AI agent (setup)\"). Quote the passage as plain text: the server mints the note's highlight and places it in the language's staged copy and in live, wherever the copy holds the passage exactly once. In neither copy: 422 `note_quote_not_found`; more than once in a copy: 422 `note_quote_ambiguous`. Either way nothing is written. Placing the highlight is neither an edit nor a publish: no revision, no staged edit, nothing a reader sees. Also counts against the write rate limit (`public-api-write`, 120 requests/minute per credential by default).","tags":["Notes"],"security":[{"apiKey":["notes.write"]},{"oauth2":["notes.write"]}],"parameters":[{"$ref":"#/components/parameters/XHCioSite"},{"$ref":"#/components/parameters/SiteQuery"},{"name":"articleId","in":"path","required":true,"description":"Id of an article on the resolved help center. Not route-constrained: an id that matches no article of this help center answers 404.","schema":{"type":"integer"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["quote","note"],"properties":{"quote":{"type":"string","maxLength":5000,"description":"The passage, as plain text exactly as it reads in the article (not Markdown or HTML), long enough to be unique. Matched on its non-whitespace characters, so whitespace and entities do not matter."},"note":{"type":"string","minLength":1,"maxLength":5000,"description":"Plain text. HTML is escaped on storage."},"lang":{"type":["string","null"],"description":"One of the help center's languages; the default language when left out."}}}}}},"responses":{"201":{"description":"The new thread.","content":{"application/json":{"schema":{"type":"object","required":["status","message","note"],"properties":{"status":{"const":"success"},"message":{"const":"Note added."},"note":{"allOf":[{"$ref":"#/components/schemas/ArticleNote"},{"required":["replies"]}]}}}}}},"400":{"$ref":"#/components/responses/SiteRequired"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanUpgradeRequired"},"403":{"description":"Refused. Codes: `insufficient_scope` (the credential was not granted the notes scope — content scopes do not imply it), `role_forbidden` (OAuth: the person's role on this help center does not allow it, e.g. a Viewer), `site_forbidden` (the selected help center is not part of the account), `notes_forbidden` (the user behind the credential no longer holds the `notes.read` permission on this help center, re-checked on every request), or no code with the message \"You are not allowed to create editorial notes with this credential. Creating one needs the notes.write scope, which is granted separately from content access.\".","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/InsufficientScopeError"},{"$ref":"#/components/schemas/RoleForbiddenError"},{"$ref":"#/components/schemas/SiteForbiddenError"},{"$ref":"#/components/schemas/Error"},{"$ref":"#/components/schemas/NotesForbiddenError"}]}}}},"404":{"$ref":"#/components/responses/NotFound"},"422":{"description":"Validation failed (`status: validation_error`), or the quote cannot be placed (`note_quote_not_found`, `note_quote_ambiguous` with `occurrences`). Nothing was written.","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/ValidationError"},{"$ref":"#/components/schemas/NoteRefusedError"}]}}}},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/v1/articles/{articleId}/notes/{noteId}/replies":{"post":{"operationId":"replyToArticleNote","summary":"Reply to an editorial note","description":"Adds an internal reply to a top-level note, attributed to the user behind the credential and written in the note's own language. A resolved note, a reply's id and a reader comment's id all answer 404. Also counts against the write rate limit (`public-api-write`, 120 requests/minute per credential by default).","tags":["Notes"],"security":[{"apiKey":["notes.write"]},{"oauth2":["notes.write"]}],"parameters":[{"$ref":"#/components/parameters/XHCioSite"},{"$ref":"#/components/parameters/SiteQuery"},{"name":"articleId","in":"path","required":true,"description":"Id of an article on the resolved help center. Not route-constrained: an id that matches no article of this help center answers 404.","schema":{"type":"integer"}},{"name":"noteId","in":"path","required":true,"description":"Id of a top-level editorial note on this article.","schema":{"type":"integer"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["note"],"properties":{"note":{"type":"string","minLength":1,"maxLength":5000,"description":"Plain text. HTML is escaped on storage."}}}}}},"responses":{"201":{"description":"The whole thread: the note with its open replies, including the new one.","content":{"application/json":{"schema":{"type":"object","required":["status","message","note"],"properties":{"status":{"const":"success"},"message":{"const":"Reply added to the note."},"note":{"allOf":[{"$ref":"#/components/schemas/ArticleNote"},{"required":["replies"]}]}}}}}},"400":{"$ref":"#/components/responses/SiteRequired"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanUpgradeRequired"},"403":{"description":"Refused. Codes: `insufficient_scope` (the credential was not granted the notes scope — content scopes do not imply it), `role_forbidden` (OAuth: the person's role on this help center does not allow it, e.g. a Viewer), `site_forbidden` (the selected help center is not part of the account), `notes_forbidden` (the user behind the credential no longer holds the `notes.read` permission on this help center, re-checked on every request), or no code with the message \"You are not allowed to reply to editorial notes with this credential. Replying needs the notes.write scope, which is granted separately from content access.\".","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/InsufficientScopeError"},{"$ref":"#/components/schemas/RoleForbiddenError"},{"$ref":"#/components/schemas/SiteForbiddenError"},{"$ref":"#/components/schemas/Error"},{"$ref":"#/components/schemas/NotesForbiddenError"}]}}}},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/ValidationFailed"},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/v1/articles/{articleId}/notes/{noteId}/resolve":{"post":{"operationId":"resolveArticleNote","summary":"Resolve an editorial note","description":"Resolves a top-level note: soft-deletes it and its replies and removes its highlight from the article's live content and staged version in the note's language, unless another open note uses the same highlight. It stays readable with `include_resolved=1`, and an already-resolved note answers 200 with \"That note was already resolved.\". Takes no request body. Also counts against the write rate limit (`public-api-write`, 120 requests/minute per credential by default).","tags":["Notes"],"security":[{"apiKey":["notes.write"]},{"oauth2":["notes.write"]}],"parameters":[{"$ref":"#/components/parameters/XHCioSite"},{"$ref":"#/components/parameters/SiteQuery"},{"name":"articleId","in":"path","required":true,"description":"Id of an article on the resolved help center. Not route-constrained: an id that matches no article of this help center answers 404.","schema":{"type":"integer"}},{"name":"noteId","in":"path","required":true,"description":"Id of a top-level editorial note on this article.","schema":{"type":"integer"}}],"responses":{"200":{"description":"Success.","content":{"application/json":{"schema":{"type":"object","required":["status","message","id"],"properties":{"status":{"const":"success"},"message":{"type":"string","enum":["Note resolved.","That note was already resolved."]},"id":{"type":"integer"}}}}}},"400":{"$ref":"#/components/responses/SiteRequired"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanUpgradeRequired"},"403":{"description":"Refused. Codes: `insufficient_scope` (the credential was not granted the notes scope — content scopes do not imply it), `role_forbidden` (OAuth: the person's role on this help center does not allow it, e.g. a Viewer), `site_forbidden` (the selected help center is not part of the account), `notes_forbidden` (the user behind the credential no longer holds the `notes.read` permission on this help center, re-checked on every request).","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/InsufficientScopeError"},{"$ref":"#/components/schemas/RoleForbiddenError"},{"$ref":"#/components/schemas/SiteForbiddenError"},{"$ref":"#/components/schemas/Error"},{"$ref":"#/components/schemas/NotesForbiddenError"}]}}}},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/v1/images":{"get":{"operationId":"listImages","summary":"List images","description":"The site's images uploaded through the public API, newest first (by id). The editor's file library is a separate inventory and is not listed. `alt_text` reads back as an empty string, since it is not stored with the image.","tags":["Images"],"security":[{"apiKey":["content.read"]},{"oauth2":["content.read"]}],"parameters":[{"$ref":"#/components/parameters/XHCioSite"},{"$ref":"#/components/parameters/SiteQuery"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100,"default":50},"description":"Images per page."},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":1},"description":"1-based page number."},{"name":"checksum","in":"query","required":false,"schema":{"type":"string","maxLength":64},"description":"Only images whose sha256 `checksum` equals this value, so a client can confirm an upload landed without listing everything."}],"responses":{"200":{"description":"A page of images.","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","const":"success"},"images":{"type":"array","items":{"$ref":"#/components/schemas/Image"}},"meta":{"type":"object","properties":{"page":{"type":"integer"},"per_page":{"type":"integer","description":"The effective `limit`."},"total_pages":{"type":"integer","minimum":1,"description":"At least 1, even when there are no images."},"items_count":{"type":"integer","description":"Total images matching the query, across all pages."}},"required":["page","per_page","total_pages","items_count"]}},"required":["status","images","meta"]}}}},"400":{"$ref":"#/components/responses/SiteRequired"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanUpgradeRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound","description":"The API key's site has been deleted (`The site this API key belongs to is no longer available.`)."},"422":{"$ref":"#/components/responses/ValidationFailed","description":"`limit`, `page` or `checksum` failed validation (`message`: `The request could not be accepted.`)."},"429":{"$ref":"#/components/responses/TooManyRequests"}}},"post":{"operationId":"uploadImage","summary":"Upload an image","description":"Stores one image, sent as a multipart `file` or downloaded by the server from an https `source_url`, and returns its stable absolute URL to embed in article content. On a help center readers need permission for (not public, or behind an IP allow-list) the image is private: its URL opens only for the help center's team, and the pages that show it give readers a signed copy that expires. Idempotent by content: with `dedupe` on (the default), bytes this site has already uploaded through the API answer 200 with `reused: true` and the existing image (its id, URL and filename) instead of 201 and a new copy. Throttled at 60 requests per minute per client IP, on top of the API-wide limits. While the help center is unclaimed, each image stored counts toward the 500 MB of images and files it may store; one that does not fit is refused with `422 limit_reached`.","tags":["Images"],"security":[{"apiKey":["content.write"]},{"oauth2":["content.write"]}],"parameters":[{"$ref":"#/components/parameters/XHCioSite"},{"$ref":"#/components/parameters/SiteQuery"}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","properties":{"file":{"type":"string","contentMediaType":"application/octet-stream","description":"The image: JPEG, PNG, GIF or WebP (image/jpeg, image/png, image/gif, image/webp), detected from the file's bytes rather than its name or Content-Type; SVG is refused; at most 10 MB (10,485,760 bytes; `max_kb` 10240 in the default configuration). Required unless `source_url` is sent; sending both is refused."},"source_url":{"type":"string","format":"uri","maxLength":2048,"description":"An https address the server downloads the image from. Addresses in private or reserved ranges, or on a port other than 80 or 443, are refused, every redirect is re-checked and must stay on https, and the download must finish within 15 seconds. The bytes must be JPEG, PNG, GIF or WebP (image/jpeg, image/png, image/gif, image/webp), detected from the file's bytes rather than its name or Content-Type; SVG is refused, at most 10 MB (10,485,760 bytes; `max_kb` 10240 in the default configuration). Required unless `file` is sent; sending both is refused."},"alt_text":{"type":["string","null"],"maxLength":255,"description":"Alt text for the returned `snippets`; trimmed. Not stored with the image."},"dedupe":{"type":"string","enum":["0","1"],"default":"1","description":"Whether bytes this site has already uploaded through the API resolve to the existing image instead of a second copy. On when omitted. Only `0` and `1` pass validation in a form body (`true`/`false` are refused); an empty value turns deduplication off."}}}},"application/json":{"schema":{"type":"object","properties":{"source_url":{"type":"string","format":"uri","maxLength":2048,"description":"An https address the server downloads the image from. Addresses in private or reserved ranges, or on a port other than 80 or 443, are refused, every redirect is re-checked and must stay on https, and the download must finish within 15 seconds. The bytes must be JPEG, PNG, GIF or WebP (image/jpeg, image/png, image/gif, image/webp), detected from the file's bytes rather than its name or Content-Type; SVG is refused, at most 10 MB (10,485,760 bytes; `max_kb` 10240 in the default configuration)."},"alt_text":{"type":["string","null"],"maxLength":255,"description":"Alt text for the returned `snippets`; trimmed. Not stored with the image."},"dedupe":{"type":["boolean","null"],"default":true,"description":"Whether bytes this site has already uploaded through the API resolve to the existing image instead of a second copy. Defaults to on when omitted. Also accepts 0, 1, \"0\" and \"1\". An explicit null turns deduplication off."}},"required":["source_url"]}}}},"responses":{"200":{"description":"The bytes matched an image this site already holds; that image is returned with `reused: true`, decorated with this request's `alt_text`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ImagesStoreResult"}}}},"201":{"description":"The image was stored (`reused: false`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ImagesStoreResult"}}}},"400":{"$ref":"#/components/responses/SiteRequired"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanUpgradeRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound","description":"The API key's site has been deleted (`The site this API key belongs to is no longer available.`)."},"413":{"description":"The request body is larger than the 32 MB request-size ceiling (PHP `post_max_size`), so it is refused before validation and nothing is stored. Batch by total bytes, not only by count.","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}},"422":{"description":"Either validation failed (`message`: `The upload could not be accepted.`) — no `file` and no `source_url`, both, an unsupported type, a file over the size limit, or a `source_url` that is not https — or the `source_url` could not be imported (`message`: `The image at source_url could not be imported.`, with `errors.source_url[0]` reading `The image could not be imported: <reason>.`: a private or reserved address, a port other than 80 or 443, a non-200 answer, a redirect off https, too large, not an allowed image type, or too slow). Or `limit_reached` (`limit`: `storage_bytes`): the help center is unclaimed and the image does not fit in the 500 MB of images and files it may store; nothing was stored.","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/ValidationError"},{"$ref":"#/components/schemas/LimitReachedError"}]}}}},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/v1/images/bulk":{"post":{"operationId":"bulkUploadImages","summary":"Upload several images","description":"Stores up to 20 images (default configuration) in one request: files, https source URLs, or both. Per-item rules are validated first, so a batch with any invalid item is refused whole with 422 and nothing stored. Past validation, always answers 207 with one outcome per item in index order — `created`, `reused` (deduplicated, as on POST /v1/images) or `error` (a download that failed, a storage failure, or `limit_reached`: on an unclaimed help center, an image that does not fit in the 500 MB of images and files it may store; each such item fails on its own, and the ones that fit land) — and one failed item does not stop the rest. Throttled at 20 requests per minute per client IP, on top of the API-wide limits; PHP's 32 MB request ceiling is usually the binding limit for files.","tags":["Images"],"security":[{"apiKey":["content.write"]},{"oauth2":["content.write"]}],"parameters":[{"$ref":"#/components/parameters/XHCioSite"},{"$ref":"#/components/parameters/SiteQuery"}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","properties":{"files":{"type":"array","minItems":1,"maxItems":20,"items":{"type":"string","contentMediaType":"application/octet-stream"},"description":"Images sent as `files[]` or `files[<index>]`; each must be JPEG, PNG, GIF or WebP (image/jpeg, image/png, image/gif, image/webp), detected from the file's bytes rather than its name or Content-Type; SVG is refused, at most 10 MB (10,485,760 bytes; `max_kb` 10240 in the default configuration). Required unless `source_urls` is sent. Items are positional: `files[i]` is item i, and one index may not carry both a file and a source URL. Files and source URLs together count against the batch limit of 20 (default configuration)."},"source_urls":{"type":"array","minItems":1,"maxItems":20,"items":{"type":"string","format":"uri","maxLength":2048},"description":"https addresses the server downloads, as `source_urls[<index>]` in a form body or an array in JSON. Same rules as `source_url` on POST /v1/images, with one 45-second download budget shared by the whole batch. Required unless `files` is sent."},"alt_texts":{"type":"array","items":{"type":["string","null"],"maxLength":255},"description":"Alt text per item, matched by index (`alt_texts[i]` belongs to item i); trimmed. Used for that item's `snippets` only."},"dedupe":{"type":"string","enum":["0","1"],"default":"1","description":"Whether bytes this site has already uploaded through the API resolve to the existing image instead of a second copy. On when omitted. Only `0` and `1` pass validation in a form body (`true`/`false` are refused); an empty value turns deduplication off."}}}},"application/json":{"schema":{"type":"object","properties":{"source_urls":{"type":"array","minItems":1,"maxItems":20,"items":{"type":"string","format":"uri","maxLength":2048},"description":"https addresses the server downloads, as `source_urls[<index>]` in a form body or an array in JSON. Same rules as `source_url` on POST /v1/images, with one 45-second download budget shared by the whole batch."},"alt_texts":{"type":["array","null"],"items":{"type":["string","null"],"maxLength":255},"description":"Alt text per item, matched by index (`alt_texts[i]` belongs to item i); trimmed. Used for that item's `snippets` only."},"dedupe":{"type":["boolean","null"],"default":true,"description":"Whether bytes this site has already uploaded through the API resolve to the existing image instead of a second copy. Defaults to on when omitted. Also accepts 0, 1, \"0\" and \"1\". An explicit null turns deduplication off."}},"required":["source_urls"]}}}},"responses":{"207":{"description":"Per-item outcomes.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ImagesBulkResult"}}}},"400":{"$ref":"#/components/responses/SiteRequired"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanUpgradeRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound","description":"The API key's site has been deleted (`The site this API key belongs to is no longer available.`)."},"413":{"description":"The request body is larger than the 32 MB request-size ceiling (PHP `post_max_size`), so it is refused before validation and nothing is stored. Batch by total bytes, not only by count.","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}},"422":{"$ref":"#/components/responses/ValidationFailed","description":"The batch was refused whole (`message`: `The upload could not be accepted.`). Errors are keyed per item (`files.<i>`, `source_urls.<i>`) for an invalid file, a non-https address, or an index carrying both a file and a source URL; a batch over the limit is reported under `files` / `source_urls`, and under `source_urls` when files and source URLs together exceed it."},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/v1/images/uploads":{"post":{"operationId":"createImageUploadUrl","summary":"Create a single-use upload URL","description":"Mints a URL that accepts one image upload into this site, attributed to this credential's user, with no credential of its own — for handing to whatever holds the file. Spend it with POST /v1/images/uploads/{intent}. It expires after 15 minutes (default configuration), is spent by the first upload that is stored or deduplicated, and stops working if the minting credential loses write access to the site. Takes no request body. Throttled at 60 requests per minute per client IP, on top of the API-wide limits. While the help center is unclaimed and already stores all the images and files it may, no URL is minted (`422 limit_reached`).","tags":["Images"],"security":[{"apiKey":["content.write"]},{"oauth2":["content.write"]}],"parameters":[{"$ref":"#/components/parameters/XHCioSite"},{"$ref":"#/components/parameters/SiteQuery"}],"responses":{"201":{"description":"The upload URL.","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","const":"success"},"upload_url":{"type":"string","format":"uri","description":"Absolute URL on the API host, signed over the whole URL. Use it exactly as returned: any change, including an added query parameter or a different scheme, invalidates it."},"expires_at":{"type":"string","format":"date-time","description":"ISO 8601 expiry, with offset."},"max_bytes":{"type":"integer","description":"Largest file the URL accepts, in bytes (10,485,760 in the default configuration)."}},"required":["status","upload_url","expires_at","max_bytes"]}}}},"400":{"$ref":"#/components/responses/SiteRequired"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanUpgradeRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound","description":"The API key's site has been deleted (`The site this API key belongs to is no longer available.`)."},"422":{"description":"`limit_reached` (`limit`: `storage_bytes`): the help center is unclaimed and already stores the 500 MB of images and files it may; nothing was minted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LimitReachedError"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/v1/images/uploads/{intent}":{"post":{"operationId":"redeemImageUploadUrl","summary":"Upload an image to a single-use upload URL","description":"Spends a URL minted by POST /v1/images/uploads. Send no credential: the signed URL is the credential, and the image is stored in the site and under the user it was minted for — credentials, `X-HCio-Site` and body fields naming another site are ignored. POST to the URL exactly as returned. Accepts the same form fields as POST /v1/images except `source_url`, and answers exactly as it does (201 stored, 200 deduplicated). A refused file (422) or a failure on our side leaves the URL usable for a retry; a stored or deduplicated image spends it. Throttled at 60 requests per minute per client IP in a bucket of its own, refused requests included.","tags":["Images"],"security":[],"parameters":[{"name":"intent","in":"path","required":true,"schema":{"type":"string"},"description":"Opaque id carried by the minted `upload_url` (40 letters and digits)."},{"name":"expires","in":"query","required":true,"schema":{"type":"integer"},"description":"Opaque: the expiry (Unix timestamp) the minted `upload_url` carries. Covered by the signature; send it unchanged."},{"name":"signature","in":"query","required":true,"schema":{"type":"string"},"description":"Opaque: the signature the minted `upload_url` carries. Send it unchanged."}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","properties":{"file":{"type":"string","contentMediaType":"application/octet-stream","description":"The image: JPEG, PNG, GIF or WebP (image/jpeg, image/png, image/gif, image/webp), detected from the file's bytes rather than its name or Content-Type; SVG is refused; at most 10 MB (10,485,760 bytes; `max_kb` 10240 in the default configuration)."},"alt_text":{"type":["string","null"],"maxLength":255,"description":"Alt text for the returned `snippets`; trimmed. Not stored with the image."},"dedupe":{"type":"string","enum":["0","1"],"default":"1","description":"Whether bytes this site has already uploaded through the API resolve to the existing image instead of a second copy. On when omitted. Only `0` and `1` pass validation in a form body (`true`/`false` are refused); an empty value turns deduplication off."}},"required":["file"]}}}},"responses":{"200":{"description":"The bytes matched an image the URL's site already holds; that image is returned with `reused: true`. The URL is spent.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ImagesStoreResult"}}}},"201":{"description":"The image was stored (`reused: false`). The URL is spent.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ImagesStoreResult"}}}},"403":{"description":"`upload_url_invalid`: the signature does not match (altered, truncated, or a query parameter or the scheme changed) or the server no longer holds the URL's intent. `upload_url_revoked`: the credential that minted the URL can no longer upload images to that site (API key deleted, downgraded to read or re-pointed; OAuth consent or team role withdrawn; site or user gone).","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/InsufficientScopeError"},{"$ref":"#/components/schemas/RoleForbiddenError"},{"$ref":"#/components/schemas/SiteForbiddenError"},{"allOf":[{"$ref":"#/components/schemas/ImagesUploadUrlError"},{"type":"object","properties":{"code":{"enum":["upload_url_invalid","upload_url_revoked"]}}}]}]}}}},"410":{"description":"`upload_url_expired`: past its expiry. `upload_url_used`: it has already accepted an upload (each URL accepts one). Mint a new one with POST /v1/images/uploads.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/ImagesUploadUrlError"},{"type":"object","properties":{"code":{"enum":["upload_url_expired","upload_url_used"]}}}]}}}},"413":{"description":"The request body is larger than the 32 MB request-size ceiling (PHP `post_max_size`), so it is refused before validation and nothing is stored. Batch by total bytes, not only by count.","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}},"422":{"description":"The file is missing, not an allowed type or over the size limit, or `alt_text` / `dedupe` is invalid (`message`: `The upload could not be accepted.`). The URL stays usable. Or `limit_reached` (`limit`: `storage_bytes`): the site is an unclaimed help center and the file does not fit in the 500 MB of images and files it may store; nothing was stored, and the URL stays usable.","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/ValidationError"},{"$ref":"#/components/schemas/LimitReachedError"}]}}}},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/v1/images/{imageId}":{"get":{"operationId":"getImage","summary":"Get an image","description":"One of the site's API-uploaded images. `alt_text` reads back as an empty string, since it is not stored with the image.","tags":["Images"],"security":[{"apiKey":["content.read"]},{"oauth2":["content.read"]}],"parameters":[{"name":"imageId","in":"path","required":true,"schema":{"type":"string"},"description":"The image's `id`. The route does not constrain the value; anything that is not one of this site's API-uploaded images answers 404."},{"$ref":"#/components/parameters/XHCioSite"},{"$ref":"#/components/parameters/SiteQuery"}],"responses":{"200":{"description":"The image.","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","const":"success"},"image":{"$ref":"#/components/schemas/Image"}},"required":["status","image"]}}}},"400":{"$ref":"#/components/responses/SiteRequired"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanUpgradeRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound","description":"No image with this id among this site's API-uploaded images — an unknown id, another site's image, or an editor file-library attachment all answer `Image not found.` Also answered when the API key's site has been deleted."},"429":{"$ref":"#/components/responses/TooManyRequests"}}},"delete":{"operationId":"deleteImage","summary":"Delete an image","description":"Deletes the image's record and its stored object, so its URL stops working. Refused with 409 while content on this site still embeds the URL — a live or trashed article, or a staged or archived version of one, in its HTML or its editor document — unless `force` is true. If the stored object cannot be removed, the record is deleted anyway, except on an unclaimed help center: there the record stays, still counting toward the 500 MB it may store, and the delete answers 500 (`The image could not be deleted. Please try again.`), so retry it.","tags":["Images"],"security":[{"apiKey":["content.write"]},{"oauth2":["content.write"]}],"parameters":[{"name":"imageId","in":"path","required":true,"schema":{"type":"string"},"description":"The image's `id`. The route does not constrain the value; anything that is not one of this site's API-uploaded images answers 404."},{"$ref":"#/components/parameters/XHCioSite"},{"$ref":"#/components/parameters/SiteQuery"},{"name":"force","in":"query","required":false,"schema":{"type":"boolean","default":false},"description":"Delete even while content references the image. Read as a boolean flag: `1`, `true`, `on` and `yes` count as true; anything else as false."}],"responses":{"200":{"description":"The image was deleted.","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","const":"success"},"deleted":{"type":"object","properties":{"id":{"type":"integer"},"url":{"type":"string","format":"uri"}},"required":["id","url"]}},"required":["status","deleted"]}}}},"400":{"$ref":"#/components/responses/SiteRequired"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanUpgradeRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound","description":"No image with this id among this site's API-uploaded images — an unknown id, another site's image, or an editor file-library attachment all answer `Image not found.` Also answered when the API key's site has been deleted."},"409":{"description":"Content on this site still embeds the image and `force` was not true. Nothing was deleted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ImagesReferencedError"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/v1/translations":{"get":{"operationId":"listTranslations","summary":"List interface strings","description":"The catalogue of editable interface strings (the help center's chrome, such as headings and widget buttons, not article content) with this help center's overrides, in the dashboard Localization page's order. Keys this help center has a stored value for but the catalogue no longer lists are appended with `catalogued: false`, unless `group` is given.","tags":["Translations"],"security":[{"apiKey":["content.read"]},{"oauth2":["content.read"]}],"parameters":[{"$ref":"#/components/parameters/XHCioSite"},{"$ref":"#/components/parameters/SiteQuery"},{"name":"lang","in":"query","required":false,"description":"Narrow each entry's `text` to this one locale. Any supported language code is accepted, not only the help center's enabled languages.","schema":{"$ref":"#/components/schemas/TranslationsLocaleCode"}},{"name":"group","in":"query","required":false,"description":"Only catalogue entries whose `group` equals this value. Uncatalogued stored keys are then left out.","schema":{"type":"string","maxLength":190}},{"name":"only_overridden","in":"query","required":false,"description":"`1` to return only keys that have an override (in `lang`, when given). Laravel's `boolean` rule: `1`/`0` are accepted, the strings `true`/`false` are a 422.","schema":{"type":"integer","enum":[0,1]}}],"responses":{"200":{"description":"The interface strings and this help center's overrides.","content":{"application/json":{"schema":{"type":"object","required":["status","translations","meta"],"properties":{"status":{"const":"success"},"translations":{"type":"array","items":{"$ref":"#/components/schemas/TranslationsInterfaceString"}},"meta":{"type":"object","required":["items_count","overridden_count","default_language","site_languages"],"properties":{"items_count":{"type":"integer","description":"Number of entries in `translations`."},"overridden_count":{"type":"integer","description":"Entries whose `text` is not empty."},"default_language":{"type":"string","description":"The help center's default language code."},"site_languages":{"type":"array","items":{"type":"string"},"description":"The default language followed by the help center's other enabled languages."}}}}}}}},"400":{"$ref":"#/components/responses/SiteRequired"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanUpgradeRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound","description":"The site this API key belongs to is no longer available."},"422":{"$ref":"#/components/responses/ValidationFailed","description":"Invalid filter. An unknown `lang` carries \"Unsupported language code.\""},"429":{"$ref":"#/components/responses/TooManyRequests"}}},"post":{"operationId":"updateTranslations","summary":"Write several interface strings","description":"Merges a locale map into each named key's overrides: locales you send are set, `null` or `\"\"` removes that locale's override, and locales you do not send are left alone. A key whose last locale is removed loses its stored row. Writable keys are the catalogued ones plus any this help center already has a value for; one invalid entry fails the whole call and nothing is written. Writes are accepted even when they will not render (an `en` value for a `widget.*` key, or a language not enabled on the help center) and are then reported in `meta.warnings`.","tags":["Translations"],"security":[{"apiKey":["content.write"]},{"oauth2":["content.write"]}],"parameters":[{"$ref":"#/components/parameters/XHCioSite"},{"$ref":"#/components/parameters/SiteQuery"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["translations"],"properties":{"translations":{"type":"object","description":"Interface-string key (as listed by `GET /v1/translations`, e.g. dotted keys) to a locale map. A key that is neither catalogued nor already stored for this help center is a 422.","minProperties":1,"maxProperties":250,"additionalProperties":{"$ref":"#/components/schemas/TranslationsLocaleMapInput"}}}}}}},"responses":{"200":{"description":"The keys as stored after the merge, in the order sent.","content":{"application/json":{"schema":{"type":"object","required":["status","translations","meta"],"properties":{"status":{"const":"success"},"translations":{"type":"array","items":{"$ref":"#/components/schemas/TranslationsInterfaceString"}},"meta":{"type":"object","required":["items_count","keys_written","keys_removed","warnings"],"properties":{"items_count":{"type":"integer","description":"Number of keys in the request."},"keys_written":{"type":"integer","description":"Keys whose stored value was created or changed."},"keys_removed":{"type":"integer","description":"Keys whose stored row was deleted because no locale was left."},"warnings":{"$ref":"#/components/schemas/TranslationsWriteWarnings"}}}}}}}},"400":{"$ref":"#/components/responses/SiteRequired"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanUpgradeRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound","description":"The site this API key belongs to is no longer available."},"422":{"$ref":"#/components/responses/ValidationFailed","description":"Malformed body or entry. Errors are keyed `translations`, `translations.<key>` (e.g. \"Unknown interface-string key \\\"<key>\\\". Writable keys are the ones GET /v1/translations lists.\", or a map that is not a non-empty object) and `translations.<key>.<locale>` (\"Unsupported language code \\\"<locale>\\\".\", \"The translation must be a string, or null to remove the override.\", \"The translation may not be longer than 2000 characters.\"). More than 250 keys: \"Write at most 250 interface strings per call.\""},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/v1/translations/{translationKey}":{"post":{"operationId":"updateTranslation","summary":"Write one interface string","description":"Merges a locale map into one key's overrides, with the same rules as the bulk endpoint: `null` or `\"\"` removes a locale, unsent locales are left alone, and a key with no locale left loses its stored row. Answers 200 whether the row was created, changed, removed or left unchanged.","tags":["Translations"],"security":[{"apiKey":["content.write"]},{"oauth2":["content.write"]}],"parameters":[{"$ref":"#/components/parameters/XHCioSite"},{"$ref":"#/components/parameters/SiteQuery"},{"name":"translationKey","in":"path","required":true,"description":"The interface-string key, as listed by `GET /v1/translations`. Must be catalogued or already stored for this help center, otherwise 404.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["text"],"properties":{"text":{"$ref":"#/components/schemas/TranslationsLocaleMapInput"}}}}}},"responses":{"200":{"description":"The key as stored after the merge.","content":{"application/json":{"schema":{"type":"object","required":["status","translation","meta"],"properties":{"status":{"const":"success"},"translation":{"$ref":"#/components/schemas/TranslationsInterfaceString"},"meta":{"type":"object","required":["keys_written","keys_removed","warnings"],"properties":{"keys_written":{"type":"integer","enum":[0,1],"description":"1 when the stored value was created or changed."},"keys_removed":{"type":"integer","enum":[0,1],"description":"1 when the stored row was deleted because no locale was left."},"warnings":{"$ref":"#/components/schemas/TranslationsWriteWarnings"}}}}}}}},"400":{"$ref":"#/components/responses/SiteRequired"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanUpgradeRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound","description":"The key is neither catalogued nor stored for this help center (message \"Unknown interface-string key \\\"<key>\\\". Writable keys are the ones GET /v1/translations lists.\"), or the site this API key belongs to is no longer available."},"422":{"$ref":"#/components/responses/ValidationFailed","description":"Malformed body. Errors are keyed `text` (\"Send the text as a { \\\"locale\\\": \\\"string\\\" } object.\", \"text must be an object keyed by language code.\") and `text.<locale>` (\"Unsupported language code \\\"<locale>\\\".\", \"The translation must be a string, or null to remove the override.\", \"The translation may not be longer than 2000 characters.\"). The body is validated before the key is looked up."},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/v1/webhooks":{"get":{"operationId":"listWebhooks","summary":"List webhook subscriptions","description":"With an API key: every target registered through an API key of the key's own site (never an OAuth app's target on that site). With OAuth: the account's targets on every granted help center where the person may manage webhooks (owner or admin), whichever site the request names. The signing secret is never listed.","tags":["Webhooks"],"security":[{"apiKey":["webhooks.manage"]},{"oauth2":["webhooks.manage"]}],"x-across-granted-sites":true,"responses":{"200":{"description":"The subscriptions this credential may manage, ordered by id.","content":{"application/json":{"schema":{"type":"object","required":["status","webhooks","meta"],"properties":{"status":{"const":"success"},"webhooks":{"type":"array","items":{"$ref":"#/components/schemas/WebhookSubscription"}},"meta":{"type":"object","required":["items_count"],"properties":{"items_count":{"type":"integer"}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanUpgradeRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"}}},"post":{"operationId":"createWebhook","summary":"Register a webhook","description":"Registers a target URL for the help center in context; there is no account-wide or multi-site subscription and no event filter. Every subscription receives `article.created`, `article.published`, `article.updated`, `article.deleted`, `category.created`, `category.updated`, `category.deleted`, `site.updated` (visibility, suspension or IP allow-list changed) and `site.deleted`, from dashboard and API writes alike, as a JSON POST signed with `X-HCio-Signature: sha256=<hex HMAC-SHA256 of the raw body keyed with the secret>` and carrying `X-HCio-Event` and `X-HCio-Delivery` headers. If `secret` is omitted a 48-character random one is generated; either way the secret is returned only in this response. The URL must be http(s) and publicly routable; it is re-checked and the connection pinned at every delivery, and redirects are not followed.","tags":["Webhooks"],"security":[{"apiKey":["webhooks.manage"]},{"oauth2":["webhooks.manage"]}],"parameters":[{"$ref":"#/components/parameters/XHCioSite"},{"$ref":"#/components/parameters/SiteQuery"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["url"],"properties":{"url":{"type":"string","format":"uri","maxLength":2048,"description":"The target. Must be a valid http or https URL whose host is not, and does not resolve to, a private, loopback, link-local, CGNAT or otherwise reserved address. A host that resolves to nothing is accepted here, but deliveries to it are skipped until it resolves to a public address."},"secret":{"type":"string","minLength":16,"maxLength":255,"description":"HMAC signing secret. Generated when omitted."}}}}}},"responses":{"201":{"description":"The subscription, with its secret (shown only here).","content":{"application/json":{"schema":{"type":"object","required":["status","webhook"],"properties":{"status":{"const":"success"},"webhook":{"$ref":"#/components/schemas/WebhookSubscriptionWithSecret"}}}}}},"400":{"$ref":"#/components/responses/SiteRequired"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanUpgradeRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound","description":"The site this API key belongs to is no longer available."},"409":{"description":"The help center has no owner account, so a webhook registered for it would never be delivered.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhooksSiteHasNoOwnerError"}}}},"422":{"$ref":"#/components/responses/ValidationFailed","description":"Invalid `url` or `secret`. `url` messages: \"The url field is required.\", \"The url field must be a valid URL.\", \"The url field must not be greater than 2048 characters.\", \"The url must be an http or https URL.\", \"The url must be a publicly reachable address — private, loopback, and link-local addresses are not allowed.\", \"The url must be a valid, publicly reachable URL.\" `secret` messages: \"The secret field must be a string.\", \"The secret field must be at least 16 characters.\", \"The secret field must not be greater than 255 characters.\" The body is validated before the site is resolved."},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/v1/webhooks/{webhookId}":{"delete":{"operationId":"deleteWebhook","summary":"Delete a webhook subscription","description":"Deletes a subscription among those `GET /v1/webhooks` would list for this credential; any other id, including another site's or an OAuth app's target seen from an API key, is a 404.","tags":["Webhooks"],"security":[{"apiKey":["webhooks.manage"]},{"oauth2":["webhooks.manage"]}],"x-across-granted-sites":true,"parameters":[{"name":"webhookId","in":"path","required":true,"description":"The subscription id. Not constrained by the route; a non-numeric value answers 404.","schema":{"type":"integer"}}],"responses":{"200":{"description":"The subscription was deleted.","content":{"application/json":{"schema":{"type":"object","required":["status","webhook"],"properties":{"status":{"const":"success"},"webhook":{"type":"object","required":["id","deleted"],"properties":{"id":{"type":"integer"},"deleted":{"const":true}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanUpgradeRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound","description":"No subscription with this id that this credential may manage (message \"Webhook subscription not found.\")."},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/v1/analytics/content":{"get":{"operationId":"getAnalyticsContent","summary":"Get per-article performance","description":"Per-article views and rating shares for the window, sorted and paginated. An article nobody opened in the window has no row (`excludes_never_viewed` is always true), and drafts are never reported. For an OAuth Viewer or Translator, a row about an article the dashboard keeps from them keeps its numbers but reports `title`, `slug`, `updated_at` and `_links.view` as null and `status` as `private`. Shares the analytics rate limit and 300-second response cache described on the summary endpoint.","tags":["Analytics"],"security":[{"apiKey":["analytics.read"]},{"oauth2":["analytics.read"]}],"parameters":[{"$ref":"#/components/parameters/XHCioSite"},{"$ref":"#/components/parameters/SiteQuery"},{"name":"from","in":"query","required":false,"description":"First day of the window. Any value Laravel's `date` rule accepts (use `YYYY-MM-DD`); only the calendar day is used. Defaults to 6 days before `to`, i.e. a 7-day window.","schema":{"type":"string"}},{"name":"to","in":"query","required":false,"description":"Last day of the window, inclusive. Must not be before `from`. Defaults to today. The whole window may span at most 92 days; a longer one is refused with 422, never truncated.","schema":{"type":"string"}},{"name":"lang","in":"query","required":false,"description":"Restrict to one language. Must be one of the help center's enabled languages (its default or an additional language). Omit to report all languages together.","schema":{"type":"string","maxLength":16}},{"name":"category_id","in":"query","required":false,"description":"Only articles in this category. Must be a category of this help center (another tenant's id is a 422). The sentinels `0` and `-1` are also accepted and, like a category holding no articles, return an empty report.","schema":{"type":"integer"}},{"name":"sort","in":"query","required":false,"description":"`views_desc`: most views in the window first. `views_asc`: fewest first (among articles that were viewed at all). `rating_asc`: highest thumbs-down share first, ties broken towards more views.","schema":{"type":"string","enum":["views_desc","views_asc","rating_asc"],"default":"views_desc"}},{"name":"page","in":"query","required":false,"description":"Page number. A page past the last returns an empty `articles` list.","schema":{"type":"integer","minimum":1,"default":1}},{"name":"per_page","in":"query","required":false,"description":"Rows per page.","schema":{"type":"integer","minimum":1,"maximum":100,"default":25}}],"responses":{"200":{"description":"One page of per-article performance rows.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnalyticsContentResponse"}}}},"400":{"$ref":"#/components/responses/SiteRequired"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanUpgradeRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound","description":"The site this API key belongs to is no longer available."},"422":{"$ref":"#/components/responses/ValidationFailed","description":"Invalid window, language, category, sort or paging value. Besides the summary endpoint's messages, `category_id` may carry \"That category does not belong to this help center.\""},"429":{"description":"Rate limited. Either the analytics ceiling was hit (body `code: analytics_rate_limited`; 6 requests per minute per credential by default, shared by all three analytics endpoints) or the general /v1 read ceiling was (bare `{message}` body). The figures are computed daily, so wait rather than retrying in a loop.","headers":{"Retry-After":{"description":"Seconds until the bucket that was hit frees up.","schema":{"type":"integer"}},"X-RateLimit-Limit":{"description":"Requests per minute allowed by the bucket that was hit.","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests left in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp at which the bucket frees up.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/RateLimitError"},{"$ref":"#/components/schemas/AnalyticsRateLimitedError"}]}}}},"503":{"description":"The analytics backend failed. This is not a problem with the request and retrying immediately will not help; wait for `Retry-After`. Failures are never cached.","headers":{"Retry-After":{"description":"Always 300.","schema":{"type":"integer","const":300}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnalyticsUnavailableError"}}}}}}},"/v1/analytics/searches":{"get":{"operationId":"getAnalyticsSearches","summary":"Get search demand and content gaps","description":"Top-N rollups of what visitors searched for: `content_gaps` (queries that returned nothing or that nobody clicked, worst first), `frequent_queries` (by volume) and `common_terms` (single words). The lists are top-N, not pages, and no search totals are reported here; use `summary.traffic.searches` from the summary endpoint. For an OAuth Viewer or Translator, a `top_hit` or `article_clicked` they may not see is reduced to `{article_id, status: \"private\"}`. Shares the analytics rate limit and 300-second response cache described on the summary endpoint.","tags":["Analytics"],"security":[{"apiKey":["analytics.read"]},{"oauth2":["analytics.read"]}],"parameters":[{"$ref":"#/components/parameters/XHCioSite"},{"$ref":"#/components/parameters/SiteQuery"},{"name":"from","in":"query","required":false,"description":"First day of the window. Any value Laravel's `date` rule accepts (use `YYYY-MM-DD`); only the calendar day is used. Defaults to 6 days before `to`, i.e. a 7-day window.","schema":{"type":"string"}},{"name":"to","in":"query","required":false,"description":"Last day of the window, inclusive. Must not be before `from`. Defaults to today. The whole window may span at most 92 days; a longer one is refused with 422, never truncated.","schema":{"type":"string"}},{"name":"lang","in":"query","required":false,"description":"Restrict to one language. Must be one of the help center's enabled languages (its default or an additional language). Omit to report all languages together.","schema":{"type":"string","maxLength":16}},{"name":"limit","in":"query","required":false,"description":"Maximum rows in each of the three lists.","schema":{"type":"integer","minimum":1,"maximum":50,"default":20}}],"responses":{"200":{"description":"The search insight lists for the resolved window.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnalyticsSearchesResponse"}}}},"400":{"$ref":"#/components/responses/SiteRequired"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanUpgradeRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound","description":"The site this API key belongs to is no longer available."},"422":{"$ref":"#/components/responses/ValidationFailed","description":"Invalid window, language or `limit` (1-50). Messages as on the summary endpoint."},"429":{"description":"Rate limited. Either the analytics ceiling was hit (body `code: analytics_rate_limited`; 6 requests per minute per credential by default, shared by all three analytics endpoints) or the general /v1 read ceiling was (bare `{message}` body). The figures are computed daily, so wait rather than retrying in a loop.","headers":{"Retry-After":{"description":"Seconds until the bucket that was hit frees up.","schema":{"type":"integer"}},"X-RateLimit-Limit":{"description":"Requests per minute allowed by the bucket that was hit.","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests left in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp at which the bucket frees up.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/RateLimitError"},{"$ref":"#/components/schemas/AnalyticsRateLimitedError"}]}}}},"503":{"description":"The analytics backend failed. This is not a problem with the request and retrying immediately will not help; wait for `Retry-After`. Failures are never cached.","headers":{"Retry-After":{"description":"Always 300.","schema":{"type":"integer","const":300}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnalyticsUnavailableError"}}}}}}},"/v1/analytics/summary":{"get":{"operationId":"getAnalyticsSummary","summary":"Get the analytics summary","description":"Traffic, ratings and authoring activity for a window; each traffic figure is compared with the equally long window immediately before it. Analytics requests carry their own rate limit (6 per minute per credential by default) on top of the general read limit, and responses are reused per help center and parameters for 300 seconds by default.","tags":["Analytics"],"security":[{"apiKey":["analytics.read"]},{"oauth2":["analytics.read"]}],"parameters":[{"$ref":"#/components/parameters/XHCioSite"},{"$ref":"#/components/parameters/SiteQuery"},{"name":"from","in":"query","required":false,"description":"First day of the window. Any value Laravel's `date` rule accepts (use `YYYY-MM-DD`); only the calendar day is used. Defaults to 6 days before `to`, i.e. a 7-day window.","schema":{"type":"string"}},{"name":"to","in":"query","required":false,"description":"Last day of the window, inclusive. Must not be before `from`. Defaults to today. The whole window may span at most 92 days; a longer one is refused with 422, never truncated.","schema":{"type":"string"}},{"name":"lang","in":"query","required":false,"description":"Restrict to one language. Must be one of the help center's enabled languages (its default or an additional language). Omit to report all languages together.","schema":{"type":"string","maxLength":16}}],"responses":{"200":{"description":"The summary for the resolved window.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnalyticsSummaryResponse"}}}},"400":{"$ref":"#/components/responses/SiteRequired"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanUpgradeRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound","description":"The site this API key belongs to is no longer available."},"422":{"$ref":"#/components/responses/ValidationFailed","description":"Invalid window or language. Messages include `to`: \"The end of the range cannot be before its start.\" / \"The reporting range cannot be longer than 92 days. Ask for a shorter window, or several in sequence.\" and `lang`: \"That language is not enabled on this help center.\""},"429":{"description":"Rate limited. Either the analytics ceiling was hit (body `code: analytics_rate_limited`; 6 requests per minute per credential by default, shared by all three analytics endpoints) or the general /v1 read ceiling was (bare `{message}` body). The figures are computed daily, so wait rather than retrying in a loop.","headers":{"Retry-After":{"description":"Seconds until the bucket that was hit frees up.","schema":{"type":"integer"}},"X-RateLimit-Limit":{"description":"Requests per minute allowed by the bucket that was hit.","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests left in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp at which the bucket frees up.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/RateLimitError"},{"$ref":"#/components/schemas/AnalyticsRateLimitedError"}]}}}},"503":{"description":"The analytics backend failed. This is not a problem with the request and retrying immediately will not help; wait for `Retry-After`. Failures are never cached.","headers":{"Retry-After":{"description":"Always 300.","schema":{"type":"integer","const":300}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnalyticsUnavailableError"}}}}}}},"/v1/widget":{"get":{"operationId":"getWidgetInstall","summary":"Get the widget's install details","description":"What it takes to put the help center's widget on the customer's website: the widget id, the exact snippet the dashboard's Embeddables page shows, the websites allowed to embed it, whether it has been seen running, and whether visitors are served at all (an unpublished, private or suspended help center serves only some, or none). Provisions the widget on the first read, as opening that page does. Never carries the single sign-on shared secret.","tags":["Widget and FAQ sections"],"security":[{"apiKey":["content.read"]},{"oauth2":["content.read"]}],"parameters":[{"$ref":"#/components/parameters/XHCioSite"},{"$ref":"#/components/parameters/SiteQuery"}],"responses":{"200":{"description":"The install details.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WidgetResponse"}}}},"400":{"$ref":"#/components/responses/SiteRequired"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanUpgradeRequired"},"429":{"$ref":"#/components/responses/TooManyRequests"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"`widget_unavailable` when the widget is not available on this account; or the API key's help center has been deleted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/widget/embedding-origins":{"patch":{"operationId":"updateEmbeddingOrigins","summary":"Change the embedding origins","description":"Changes which websites may embed the help center and run its widget, the list the dashboard's Settings page and widget card edit too. This is a security setting: it sets the `frame-ancestors` header on every help-center page and which hosts every widget endpoint answers. `add` and `remove` edit the list in place; `replace` sends the whole list. Each added entry must be a host name (`www.example.com`), or a leading-dot wildcard over one organization's domain (`.example.com`, the domain and its subdomains); `*.example.com` is read as `.example.com`. Refused: `*`, and a wildcard over a public suffix from the Public Suffix List (publicsuffix.org): a domain ending anyone can register a name under (`.com`, `.co.uk`, `.k12.ca.us`), or a host many unrelated websites share (`.vercel.app`, `.github.io`). Entries already on the list are kept as they are. Leaving the list empty stops the widget everywhere and blanks every iframe of the help center, so it needs `allow_block_all: true`. Held to Owners and Admins on the OAuth lane. Write-limited.","tags":["Widget and FAQ sections"],"security":[{"apiKey":["embed.manage"]},{"oauth2":["embed.manage"]}],"parameters":[{"$ref":"#/components/parameters/XHCioSite"},{"$ref":"#/components/parameters/SiteQuery"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"add":{"type":"array","maxItems":50,"items":{"type":"string","maxLength":253},"description":"Entries to add.","examples":[["www.example.com",".example.io"]]},"remove":{"type":"array","maxItems":100,"items":{"type":"string","maxLength":253},"description":"Entries to remove. One the list does not hold is reported in `not_listed`."},"replace":{"type":"array","maxItems":50,"items":{"type":"string","maxLength":253},"description":"The whole list, instead of `add` and `remove`."},"allow_block_all":{"type":"boolean","description":"Confirms leaving the list empty, which no website may then embed."}}}}}},"responses":{"200":{"description":"Saved.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmbeddingOriginsResponse"}}}},"400":{"$ref":"#/components/responses/SiteRequired"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanUpgradeRequired"},"429":{"$ref":"#/components/responses/TooManyRequests"},"403":{"description":"`role_forbidden`: an OAuth token whose person is not an Owner or Admin of the help center. `insufficient_scope`, `site_forbidden` and `agent_not_allowed` (an unclaimed help center) as on every route.","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/RoleForbiddenError"},{"$ref":"#/components/schemas/InsufficientScopeError"},{"$ref":"#/components/schemas/SiteForbiddenError"},{"$ref":"#/components/schemas/AgentNotAllowedError"},{"$ref":"#/components/schemas/Error"}]}}}},"404":{"description":"The API key's help center has been deleted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"An entry was refused (the messages name it and say why), the body named nothing to change, or it would empty the list without `allow_block_all` (on `allow_block_all`). Nothing was changed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}}}}}},"/v1/faq-sections":{"get":{"operationId":"listFaqSections","summary":"List FAQ sections","description":"The help center's embeddable FAQ sections, oldest first, and the templates a section may take. Editors, Admins and Owners on the OAuth lane, as on the Embeddables page.","tags":["Widget and FAQ sections"],"security":[{"apiKey":["content.read"]},{"oauth2":["content.read"]}],"parameters":[{"$ref":"#/components/parameters/XHCioSite"},{"$ref":"#/components/parameters/SiteQuery"},{"name":"type","in":"query","required":false,"description":"Only sections of this type.","schema":{"type":["string","null"],"enum":["full","custom",null]}}],"responses":{"200":{"description":"The sections.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FaqSectionListResponse"}}}},"400":{"$ref":"#/components/responses/SiteRequired"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanUpgradeRequired"},"429":{"$ref":"#/components/responses/TooManyRequests"},"403":{"description":"`role_forbidden`: an OAuth token whose person is not an Editor, Admin or Owner of the help center, who alone may open the Embeddables page's FAQ sections. `insufficient_scope`, `site_forbidden` and `agent_not_allowed` (an unclaimed help center) as on every route.","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/RoleForbiddenError"},{"$ref":"#/components/schemas/InsufficientScopeError"},{"$ref":"#/components/schemas/SiteForbiddenError"},{"$ref":"#/components/schemas/AgentNotAllowedError"},{"$ref":"#/components/schemas/Error"}]}}}},"404":{"description":"`widget_unavailable` when the widget is not available on this account; or the API key's help center has been deleted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"createFaqSection","summary":"Create an FAQ section","description":"A new embeddable FAQ section, on the `slate` template unless `template` names another. A `custom` section starts empty: add FAQs to it next. Place its `install_snippet` on a page that also loads the widget. Admins and Owners on the OAuth lane. Write-limited.","tags":["Widget and FAQ sections"],"security":[{"apiKey":["content.write"]},{"oauth2":["content.write"]}],"parameters":[{"$ref":"#/components/parameters/XHCioSite"},{"$ref":"#/components/parameters/SiteQuery"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["type","title"],"properties":{"type":{"enum":["full","custom"]},"title":{"$ref":"#/components/schemas/FaqSectionTextInput","description":"Text, or text by language. At most 255 characters as stored JSON, every translation included."},"description":{"anyOf":[{"$ref":"#/components/schemas/FaqSectionTextInput"},{"type":"null"}],"description":"Shown under the title by the templates that show one. At most 1000 characters as stored JSON. null clears it."},"display_urls":{"type":["array","null"],"maxItems":50,"items":{"type":["string","null"],"maxLength":2048},"description":"Host names of the pages that may load the section; a URL is reduced to its host. A host matches only itself (www.example.com and example.com are two), and a leading-dot entry (`.example.com`) matches the domain and all its subdomains. The help center's own domain always may."},"template":{"enum":["classic","slate","glass","bubble","ledger","duet","pivot","atlas","haiku","trail","beacon","blok","prism","terminal","marquee","orbit","mosaic","ticket","finder","folio","neon","rail","weave","cove","postcard","bulletin","dialogue","canopy"],"description":"One of `templates.ids` from the list. A new section starts on `slate`."},"design":{"$ref":"#/components/schemas/FaqSectionDesign"},"show_heading":{"type":"boolean"},"layout":{"enum":["default","sequential"],"description":"The classic renderer's layout."}}}}}},"responses":{"201":{"description":"Created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FaqSectionResponse"}}}},"400":{"$ref":"#/components/responses/SiteRequired"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanUpgradeRequired"},"429":{"$ref":"#/components/responses/TooManyRequests"},"403":{"description":"`role_forbidden`: an OAuth token whose person is not an Admin or Owner of the help center, who alone may create, change or delete a section. `widget_not_in_plan`: the plan does not include the widget and FAQ sections. `insufficient_scope`, `site_forbidden` and `agent_not_allowed` (an unclaimed help center) as on every route.","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/RoleForbiddenError"},{"$ref":"#/components/schemas/WidgetNotInPlanError"},{"$ref":"#/components/schemas/InsufficientScopeError"},{"$ref":"#/components/schemas/SiteForbiddenError"},{"$ref":"#/components/schemas/AgentNotAllowedError"},{"$ref":"#/components/schemas/Error"}]}}}},"404":{"description":"`widget_unavailable` when the widget is not available on this account; or the API key's help center has been deleted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"$ref":"#/components/responses/ValidationFailed"}}}},"/v1/faq-sections/{sectionId}":{"get":{"operationId":"getFaqSection","summary":"Get an FAQ section","description":"One section, with the FAQs a custom section lists, in reading order, and whether each shows to visitors.","tags":["Widget and FAQ sections"],"security":[{"apiKey":["content.read"]},{"oauth2":["content.read"]}],"parameters":[{"$ref":"#/components/parameters/XHCioSite"},{"$ref":"#/components/parameters/SiteQuery"},{"name":"sectionId","in":"path","required":true,"description":"The section's `id`. Another help center's section answers 404.","schema":{"type":"string","pattern":"^[0-9]+$"}}],"responses":{"200":{"description":"The section.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FaqSectionResponse"}}}},"400":{"$ref":"#/components/responses/SiteRequired"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanUpgradeRequired"},"429":{"$ref":"#/components/responses/TooManyRequests"},"403":{"description":"`role_forbidden`: an OAuth token whose person is not an Editor, Admin or Owner of the help center, who alone may open the Embeddables page's FAQ sections. `insufficient_scope`, `site_forbidden` and `agent_not_allowed` (an unclaimed help center) as on every route.","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/RoleForbiddenError"},{"$ref":"#/components/schemas/InsufficientScopeError"},{"$ref":"#/components/schemas/SiteForbiddenError"},{"$ref":"#/components/schemas/AgentNotAllowedError"},{"$ref":"#/components/schemas/Error"}]}}}},"404":{"description":"`FAQ section not found.` for a missing or another help center's section; or `widget_unavailable` when the widget is not available on this account; or the API key's help center has been deleted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"operationId":"updateFaqSection","summary":"Change an FAQ section","description":"Changes a section's title, description, display URLs, template or design; a field left out keeps its value, and `design` replaces the stored design. Live on the customer's pages at their next load. The type cannot change: the snippet names it. Admins and Owners on the OAuth lane. Write-limited.","tags":["Widget and FAQ sections"],"security":[{"apiKey":["content.write"]},{"oauth2":["content.write"]}],"parameters":[{"$ref":"#/components/parameters/XHCioSite"},{"$ref":"#/components/parameters/SiteQuery"},{"name":"sectionId","in":"path","required":true,"description":"The section's `id`. Another help center's section answers 404.","schema":{"type":"string","pattern":"^[0-9]+$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"$ref":"#/components/schemas/FaqSectionTextInput","description":"Text, or text by language. At most 255 characters as stored JSON, every translation included."},"description":{"anyOf":[{"$ref":"#/components/schemas/FaqSectionTextInput"},{"type":"null"}],"description":"Shown under the title by the templates that show one. At most 1000 characters as stored JSON. null clears it."},"display_urls":{"type":["array","null"],"maxItems":50,"items":{"type":["string","null"],"maxLength":2048},"description":"Host names of the pages that may load the section; a URL is reduced to its host. A host matches only itself (www.example.com and example.com are two), and a leading-dot entry (`.example.com`) matches the domain and all its subdomains. The help center's own domain always may."},"template":{"enum":["classic","slate","glass","bubble","ledger","duet","pivot","atlas","haiku","trail","beacon","blok","prism","terminal","marquee","orbit","mosaic","ticket","finder","folio","neon","rail","weave","cove","postcard","bulletin","dialogue","canopy"],"description":"One of `templates.ids` from the list. A new section starts on `slate`."},"design":{"$ref":"#/components/schemas/FaqSectionDesign"},"show_heading":{"type":"boolean"},"layout":{"enum":["default","sequential"],"description":"The classic renderer's layout."}}}}}},"responses":{"200":{"description":"Saved.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FaqSectionResponse"}}}},"400":{"$ref":"#/components/responses/SiteRequired"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanUpgradeRequired"},"429":{"$ref":"#/components/responses/TooManyRequests"},"403":{"description":"`role_forbidden`: an OAuth token whose person is not an Admin or Owner of the help center, who alone may create, change or delete a section. `widget_not_in_plan`: the plan does not include the widget and FAQ sections. `insufficient_scope`, `site_forbidden` and `agent_not_allowed` (an unclaimed help center) as on every route.","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/RoleForbiddenError"},{"$ref":"#/components/schemas/WidgetNotInPlanError"},{"$ref":"#/components/schemas/InsufficientScopeError"},{"$ref":"#/components/schemas/SiteForbiddenError"},{"$ref":"#/components/schemas/AgentNotAllowedError"},{"$ref":"#/components/schemas/Error"}]}}}},"404":{"description":"`FAQ section not found.`, or `widget_unavailable` when the widget is not available on this account; or the API key's help center has been deleted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"$ref":"#/components/responses/ValidationFailed"}}},"delete":{"operationId":"deleteFaqSection","summary":"Delete an FAQ section","description":"Deletes a section; pages that still carry its snippet show nothing in its place. The FAQ articles are kept. Admins and Owners on the OAuth lane. Write-limited.","tags":["Widget and FAQ sections"],"security":[{"apiKey":["content.write"]},{"oauth2":["content.write"]}],"parameters":[{"$ref":"#/components/parameters/XHCioSite"},{"$ref":"#/components/parameters/SiteQuery"},{"name":"sectionId","in":"path","required":true,"description":"The section's `id`. Another help center's section answers 404.","schema":{"type":"string","pattern":"^[0-9]+$"}}],"responses":{"200":{"description":"Deleted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FaqSectionDeletedResponse"}}}},"400":{"$ref":"#/components/responses/SiteRequired"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanUpgradeRequired"},"429":{"$ref":"#/components/responses/TooManyRequests"},"403":{"description":"`role_forbidden`: an OAuth token whose person is not an Admin or Owner of the help center, who alone may create, change or delete a section. `widget_not_in_plan`: the plan does not include the widget and FAQ sections. `insufficient_scope`, `site_forbidden` and `agent_not_allowed` (an unclaimed help center) as on every route.","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/RoleForbiddenError"},{"$ref":"#/components/schemas/WidgetNotInPlanError"},{"$ref":"#/components/schemas/InsufficientScopeError"},{"$ref":"#/components/schemas/SiteForbiddenError"},{"$ref":"#/components/schemas/AgentNotAllowedError"},{"$ref":"#/components/schemas/Error"}]}}}},"404":{"description":"`FAQ section not found.`, or `widget_unavailable` when the widget is not available on this account; or the API key's help center has been deleted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/faq-sections/{sectionId}/articles":{"post":{"operationId":"addFaqSectionArticles","summary":"Add FAQs to a section","description":"Adds FAQ articles (type `faq`) of this help center to the end of a custom section, in the order given; one it already lists stays where it is. A section lists at most 1000 FAQs. Live at once: a published FAQ shows on the customer's pages on their next load. Editors, Admins and Owners on the OAuth lane. Write-limited.","tags":["Widget and FAQ sections"],"security":[{"apiKey":["content.write"]},{"oauth2":["content.write"]}],"parameters":[{"$ref":"#/components/parameters/XHCioSite"},{"$ref":"#/components/parameters/SiteQuery"},{"name":"sectionId","in":"path","required":true,"description":"The section's `id`. Another help center's section answers 404.","schema":{"type":"string","pattern":"^[0-9]+$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["article_ids"],"properties":{"article_ids":{"type":"array","minItems":1,"maxItems":100,"items":{"type":"integer","minimum":1},"description":"FAQ article ids, each once."}}}}}},"responses":{"200":{"description":"Added, or already listed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FaqSectionArticlesAddedResponse"}}}},"400":{"$ref":"#/components/responses/SiteRequired"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanUpgradeRequired"},"429":{"$ref":"#/components/responses/TooManyRequests"},"403":{"description":"`role_forbidden`: an OAuth token whose person is not an Editor, Admin or Owner of the help center. `widget_not_in_plan`: the plan does not include the widget and FAQ sections. `insufficient_scope`, `site_forbidden` and `agent_not_allowed` (an unclaimed help center) as on every route.","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/RoleForbiddenError"},{"$ref":"#/components/schemas/WidgetNotInPlanError"},{"$ref":"#/components/schemas/InsufficientScopeError"},{"$ref":"#/components/schemas/SiteForbiddenError"},{"$ref":"#/components/schemas/AgentNotAllowedError"},{"$ref":"#/components/schemas/Error"}]}}}},"404":{"description":"`FAQ section not found.`, or `widget_unavailable` when the widget is not available on this account; or the API key's help center has been deleted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`section_lists_every_faq`: a full section, which lists every published FAQ and has no list of its own.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"An id is not an article of this help center, or not an FAQ article, or the section would list more than 1000 FAQs (the most a reorder can name). Nothing was added.","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/FaqSectionArticlesRejectedError"},{"$ref":"#/components/schemas/ValidationError"}]}}}}}}},"/v1/faq-sections/{sectionId}/articles/order":{"put":{"operationId":"reorderFaqSectionArticles","summary":"Reorder a section's FAQs","description":"Sets a custom section's reading order: `article_ids` names every FAQ it lists, each once, first first. Live at the customer's next page load. Editors, Admins and Owners on the OAuth lane. Write-limited.","tags":["Widget and FAQ sections"],"security":[{"apiKey":["content.write"]},{"oauth2":["content.write"]}],"parameters":[{"$ref":"#/components/parameters/XHCioSite"},{"$ref":"#/components/parameters/SiteQuery"},{"name":"sectionId","in":"path","required":true,"description":"The section's `id`. Another help center's section answers 404.","schema":{"type":"string","pattern":"^[0-9]+$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["article_ids"],"properties":{"article_ids":{"type":"array","minItems":1,"maxItems":1000,"items":{"type":"integer","minimum":1}}}}}}},"responses":{"200":{"description":"Saved.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FaqSectionResponse"}}}},"400":{"$ref":"#/components/responses/SiteRequired"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanUpgradeRequired"},"429":{"$ref":"#/components/responses/TooManyRequests"},"403":{"description":"`role_forbidden`: an OAuth token whose person is not an Editor, Admin or Owner of the help center. `widget_not_in_plan`: the plan does not include the widget and FAQ sections. `insufficient_scope`, `site_forbidden` and `agent_not_allowed` (an unclaimed help center) as on every route.","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/RoleForbiddenError"},{"$ref":"#/components/schemas/WidgetNotInPlanError"},{"$ref":"#/components/schemas/InsufficientScopeError"},{"$ref":"#/components/schemas/SiteForbiddenError"},{"$ref":"#/components/schemas/AgentNotAllowedError"},{"$ref":"#/components/schemas/Error"}]}}}},"404":{"description":"`FAQ section not found.`, or `widget_unavailable` when the widget is not available on this account; or the API key's help center has been deleted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`section_lists_every_faq`: a full section, which lists every published FAQ and has no list of its own.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"The order leaves out a listed FAQ, names one the section does not list, or names one twice. Nothing was changed.","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/FaqSectionOrderRejectedError"},{"$ref":"#/components/schemas/ValidationError"}]}}}}}}},"/v1/faq-sections/{sectionId}/articles/{articleId}":{"delete":{"operationId":"removeFaqSectionArticle","summary":"Remove an FAQ from a section","description":"Takes an article off a custom section's list. The article itself is kept. Live at the customer's next page load. Editors, Admins and Owners on the OAuth lane. Write-limited.","tags":["Widget and FAQ sections"],"security":[{"apiKey":["content.write"]},{"oauth2":["content.write"]}],"parameters":[{"$ref":"#/components/parameters/XHCioSite"},{"$ref":"#/components/parameters/SiteQuery"},{"name":"sectionId","in":"path","required":true,"description":"The section's `id`. Another help center's section answers 404.","schema":{"type":"string","pattern":"^[0-9]+$"}},{"name":"articleId","in":"path","required":true,"description":"The article's id.","schema":{"type":"string"}}],"responses":{"200":{"description":"Removed, or not listed (`removed: false`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FaqSectionArticleRemovedResponse"}}}},"400":{"$ref":"#/components/responses/SiteRequired"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanUpgradeRequired"},"429":{"$ref":"#/components/responses/TooManyRequests"},"403":{"description":"`role_forbidden`: an OAuth token whose person is not an Editor, Admin or Owner of the help center. `widget_not_in_plan`: the plan does not include the widget and FAQ sections. `insufficient_scope`, `site_forbidden` and `agent_not_allowed` (an unclaimed help center) as on every route.","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/RoleForbiddenError"},{"$ref":"#/components/schemas/WidgetNotInPlanError"},{"$ref":"#/components/schemas/InsufficientScopeError"},{"$ref":"#/components/schemas/SiteForbiddenError"},{"$ref":"#/components/schemas/AgentNotAllowedError"},{"$ref":"#/components/schemas/Error"}]}}}},"404":{"description":"`FAQ section not found.`, or `widget_unavailable` when the widget is not available on this account; or the API key's help center has been deleted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`section_lists_every_faq`: a full section, which lists every published FAQ and has no list of its own.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/template":{"get":{"operationId":"getTemplate","summary":"Get the design status and an outline of the draft","description":"Where the v2 design stands (draft etag, live version, links to the live site, the dashboard customizer and the draft preview) and a compact outline of the draft: every component id and only the settings that differ from their defaults. If the site has no active draft, one is forked from the live version first.","tags":["Template"],"security":[{"apiKey":["design.read"]},{"oauth2":["design.read"]}],"parameters":[{"$ref":"#/components/parameters/XHCioSite"},{"$ref":"#/components/parameters/SiteQuery"}],"responses":{"200":{"description":"The design status and the draft outline.","content":{"application/json":{"schema":{"type":"object","required":["status","template","outline"],"properties":{"status":{"const":"success"},"template":{"$ref":"#/components/schemas/TemplateStatus"},"outline":{"$ref":"#/components/schemas/TemplateOutline"}}}}}},"400":{"$ref":"#/components/responses/SiteRequired"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanUpgradeRequired"},"403":{"description":"Refused. Codes: `insufficient_scope` (the credential lacks design.read / design.write; content scopes do not imply them), `role_forbidden` (OAuth lane: the signed-in person's role on this help center cannot edit the design, e.g. a translator or viewer), `design_forbidden` (the `api.design` gate: the user behind the credential can no longer edit this help center's design in the dashboard), `site_forbidden` (OAuth lane: the named site is not part of the account). The form request's own refusal carries no code (\"You are not allowed to read/change this help center's design with this credential. ...\").","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/InsufficientScopeError"},{"$ref":"#/components/schemas/RoleForbiddenError"},{"$ref":"#/components/schemas/SiteForbiddenError"},{"$ref":"#/components/schemas/Error"},{"$ref":"#/components/schemas/TemplateDesignForbiddenError"}]}}}},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"The site cannot use the template API: `template_migration_required` (still on the classic v1 template; carries `customizer_url`, where the owner migrates it) or `templating_unavailable` (the v2 Template Editor is switched off on this environment).","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/TemplateUnavailableError"},{"$ref":"#/components/schemas/TemplateMigrationRequiredError"}]}}}},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/v1/template/draft":{"get":{"operationId":"getTemplateDraft","summary":"Get the draft document, or one part of it","description":"The draft as stored, whole (`section=document`, the default) or one part of it. `data` depends on `section`: `document` the whole TemplateDocument; `brand` / `theme` / `meta` that object; `navigation` the menu items; `scripts` / `custom_css` the list, or the one item whose `id` matches; `page` one TemplatePage (regions plus page settings); `region` one TemplateRegion; `component` `{page, region, component}` for the component with that `id`; `translations` the per-locale override documents keyed by locale (an empty JSON array when there are none). If the site has no active draft, one is forked from the live version first.","tags":["Template"],"security":[{"apiKey":["design.read"]},{"oauth2":["design.read"]}],"parameters":[{"$ref":"#/components/parameters/XHCioSite"},{"$ref":"#/components/parameters/SiteQuery"},{"name":"section","in":"query","required":false,"schema":{"type":["string","null"],"enum":["document","brand","theme","meta","navigation","scripts","custom_css","page","region","component","translations",null]},"description":"Which part to return. Omitted or empty means `document`."},{"name":"page","in":"query","required":false,"schema":{"type":["string","null"],"enum":["home","category","article","search","contact","notFound",null]},"description":"Required for `section=page` and `section=region`. With `section=component`, limits the search to that page."},{"name":"region","in":"query","required":false,"schema":{"type":["string","null"],"enum":["header","hero","sidebar","main","footer","utility",null]},"description":"Required for `section=region`."},{"name":"id","in":"query","required":false,"schema":{"type":["string","null"],"maxLength":64},"description":"Required for `section=component` (the component id). For `scripts` and `custom_css`, returns only that item; omitted or empty returns the list."}],"responses":{"200":{"description":"The requested part of the draft.","content":{"application/json":{"schema":{"type":"object","required":["status","section","etag","data"],"properties":{"status":{"const":"success"},"section":{"type":"string","enum":["document","brand","theme","meta","navigation","scripts","custom_css","page","region","component","translations"]},"etag":{"$ref":"#/components/schemas/TemplateEtag"},"data":{"type":["object","array","null"],"description":"Shape depends on `section` (see the operation description). Null for `brand`/`theme`/`meta`/`page`/`region` when the document lacks that part."}},"if":{"properties":{"section":{"const":"document"}}},"then":{"properties":{"data":{"$ref":"#/components/schemas/TemplateDocument"}}}}}}},"400":{"$ref":"#/components/responses/SiteRequired"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanUpgradeRequired"},"403":{"description":"Refused. Codes: `insufficient_scope` (the credential lacks design.read / design.write; content scopes do not imply them), `role_forbidden` (OAuth lane: the signed-in person's role on this help center cannot edit the design, e.g. a translator or viewer), `design_forbidden` (the `api.design` gate: the user behind the credential can no longer edit this help center's design in the dashboard), `site_forbidden` (OAuth lane: the named site is not part of the account). The form request's own refusal carries no code (\"You are not allowed to read/change this help center's design with this credential. ...\").","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/InsufficientScopeError"},{"$ref":"#/components/schemas/RoleForbiddenError"},{"$ref":"#/components/schemas/SiteForbiddenError"},{"$ref":"#/components/schemas/Error"},{"$ref":"#/components/schemas/TemplateDesignForbiddenError"}]}}}},"404":{"description":"`component_not_found` (no component with that id, on that page if `page` was given), `not_found` (no script or custom CSS item with that id), or the API key's site is gone (no code).","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/Error"},{"allOf":[{"$ref":"#/components/schemas/TemplateNotFoundError"},{"properties":{"code":{"enum":["component_not_found","not_found"]}}}]}]}}}},"409":{"description":"The site cannot use the template API: `template_migration_required` (still on the classic v1 template; carries `customizer_url`, where the owner migrates it) or `templating_unavailable` (the v2 Template Editor is switched off on this environment).","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/TemplateUnavailableError"},{"$ref":"#/components/schemas/TemplateMigrationRequiredError"}]}}}},"422":{"$ref":"#/components/responses/ValidationFailed"},"429":{"$ref":"#/components/responses/TooManyRequests"}}},"patch":{"operationId":"updateTemplateDraft","summary":"Apply a batch of edit operations to the draft","description":"Applies up to 50 operations to the draft in order, all-or-nothing, with the Template Editor's own rules (component registry, region sync, custom CSS compile, translation overrides). The first operation that cannot be applied refuses the whole batch with 422 `template_invalid` naming the operation, path and accepted values; nothing is saved. Edits land in the draft only; visitors see nothing until POST /v1/template/publish. Strings on /v1/template/* reach the server verbatim: the global empty-string-to-null and trim middlewares are disabled for these routes (bootstrap/app.php), so \"\" and null mean different things where a setting distinguishes them. Writes also count against the tighter public-api-write limit (120 requests/minute per credential by default).","tags":["Template"],"security":[{"apiKey":["design.write"]},{"oauth2":["design.write"]}],"parameters":[{"$ref":"#/components/parameters/XHCioSite"},{"$ref":"#/components/parameters/SiteQuery"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["operations"],"properties":{"operations":{"type":"array","minItems":1,"maxItems":50,"items":{"$ref":"#/components/schemas/TemplateOperation"},"description":"A JSON array of operations. Only the envelope is checked by the validator (422 validation_error); each operation's fields are checked against the registry and the current draft (422 template_invalid). Later operations see earlier ones' effects."},"etag":{"type":["string","null"],"maxLength":100,"description":"Optimistic lock: the draft etag from your last read (`template.draft.etag`, or `etag` from GET /v1/template/draft). When given and the draft has changed since, nothing is changed and the answer is 412 `stale_draft`. Omit it or send null to act on the current draft under a row lock. These routes do not trim strings or convert \"\" to null, so an empty string is compared like any other value and never matches."}}}}}},"responses":{"200":{"description":"Every operation was applied and the draft saved.","content":{"application/json":{"schema":{"type":"object","required":["status","results","warnings","template"],"properties":{"status":{"const":"success"},"results":{"type":"array","items":{"$ref":"#/components/schemas/TemplateOperationResult"},"description":"One entry per operation, in order."},"warnings":{"type":"array","items":{"type":"string"},"description":"Advice about the saved result (deduplicated), e.g. custom CSS that cannot match or scripts that run for every visitor."},"template":{"$ref":"#/components/schemas/TemplateStatus"}}}}}},"400":{"$ref":"#/components/responses/SiteRequired"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanUpgradeRequired"},"403":{"description":"Refused. Codes: `insufficient_scope` (the credential lacks design.read / design.write; content scopes do not imply them), `role_forbidden` (OAuth lane: the signed-in person's role on this help center cannot edit the design, e.g. a translator or viewer), `design_forbidden` (the `api.design` gate: the user behind the credential can no longer edit this help center's design in the dashboard), `site_forbidden` (OAuth lane: the named site is not part of the account). The form request's own refusal carries no code (\"You are not allowed to read/change this help center's design with this credential. ...\").","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/InsufficientScopeError"},{"$ref":"#/components/schemas/RoleForbiddenError"},{"$ref":"#/components/schemas/SiteForbiddenError"},{"$ref":"#/components/schemas/Error"},{"$ref":"#/components/schemas/TemplateDesignForbiddenError"}]}}}},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"The site cannot use the template API: `template_migration_required` (still on the classic v1 template; carries `customizer_url`, where the owner migrates it) or `templating_unavailable` (the v2 Template Editor is switched off on this environment).","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/TemplateUnavailableError"},{"$ref":"#/components/schemas/TemplateMigrationRequiredError"}]}}}},"412":{"description":"`stale_draft`: an `etag` was sent and the draft has changed since; nothing was changed. The body carries the draft's current `etag`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TemplateStaleDraftError"}}}},"422":{"description":"Either the envelope failed validation (`status: validation_error`, e.g. missing `operations` or more than 50), an operation could not be applied (`code: template_invalid`; nothing was saved), or an AI agent's key would have added a CSS address outside the help center (`code: agent_css_url_forbidden`; nothing was saved). Or `limit_reached` (`limit`: `storage_bytes`): the help center is unclaimed and an image given by `source_url`, or the public copy of one of its private images, does not fit in the 500 MB of images and files it may store; nothing was saved.","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/ValidationError"},{"$ref":"#/components/schemas/TemplateInvalidError"},{"$ref":"#/components/schemas/TemplateCssUrlForbiddenError"},{"$ref":"#/components/schemas/LimitReachedError"}]}}}},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/v1/template/draft/discard":{"post":{"operationId":"discardTemplateDraft","summary":"Reset the draft to the live design","description":"Discards every unpublished edit: the draft (same id) is overwritten with the live version's document and translations. Nothing visitors see changes. Writes also count against the tighter public-api-write limit (120 requests/minute per credential by default).","tags":["Template"],"security":[{"apiKey":["design.write"]},{"oauth2":["design.write"]}],"parameters":[{"$ref":"#/components/parameters/XHCioSite"},{"$ref":"#/components/parameters/SiteQuery"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"etag":{"type":["string","null"],"maxLength":100,"description":"Optimistic lock: the draft etag from your last read (`template.draft.etag`, or `etag` from GET /v1/template/draft). When given and the draft has changed since, nothing is changed and the answer is 412 `stale_draft`. Omit it or send null to act on the current draft under a row lock. These routes do not trim strings or convert \"\" to null, so an empty string is compared like any other value and never matches."}}}}}},"responses":{"200":{"description":"The draft was reset.","content":{"application/json":{"schema":{"type":"object","required":["status","template"],"properties":{"status":{"const":"success"},"template":{"$ref":"#/components/schemas/TemplateStatus"}}}}}},"400":{"$ref":"#/components/responses/SiteRequired"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanUpgradeRequired"},"403":{"description":"Refused. Codes: `insufficient_scope` (the credential lacks design.read / design.write; content scopes do not imply them), `role_forbidden` (OAuth lane: the signed-in person's role on this help center cannot edit the design, e.g. a translator or viewer), `design_forbidden` (the `api.design` gate: the user behind the credential can no longer edit this help center's design in the dashboard), `site_forbidden` (OAuth lane: the named site is not part of the account). The form request's own refusal carries no code (\"You are not allowed to read/change this help center's design with this credential. ...\").","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/InsufficientScopeError"},{"$ref":"#/components/schemas/RoleForbiddenError"},{"$ref":"#/components/schemas/SiteForbiddenError"},{"$ref":"#/components/schemas/Error"},{"$ref":"#/components/schemas/TemplateDesignForbiddenError"}]}}}},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"The site cannot use the template API: `template_migration_required` (still on the classic v1 template; carries `customizer_url`, where the owner migrates it) or `templating_unavailable` (the v2 Template Editor is switched off on this environment).","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/TemplateUnavailableError"},{"$ref":"#/components/schemas/TemplateMigrationRequiredError"}]}}}},"412":{"description":"`stale_draft`: an `etag` was sent and the draft has changed since; nothing was changed. The body carries the draft's current `etag`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TemplateStaleDraftError"}}}},"422":{"$ref":"#/components/responses/ValidationFailed"},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/v1/template/publish":{"post":{"operationId":"publishTemplate","summary":"Publish the draft","description":"Puts the draft live, archives the previously live version, records the new version and forks a fresh draft from it; a CDN purge for the site is queued. The only template call that changes what visitors see. Refused with 409 `nothing_to_publish` when the draft is identical to the live design. Writes also count against the tighter public-api-write limit (120 requests/minute per credential by default).","tags":["Template"],"security":[{"apiKey":["design.write"]},{"oauth2":["design.write"]}],"parameters":[{"$ref":"#/components/parameters/XHCioSite"},{"$ref":"#/components/parameters/SiteQuery"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":["string","null"],"maxLength":300,"description":"Version note, returned as the version's `message`. Stored verbatim (an empty string is kept as an empty string, not null)."},"etag":{"type":["string","null"],"maxLength":100,"description":"Pass the etag the customer reviewed to publish exactly that draft; 412 `stale_draft` if it changed since. Optimistic lock: the draft etag from your last read (`template.draft.etag`, or `etag` from GET /v1/template/draft). When given and the draft has changed since, nothing is changed and the answer is 412 `stale_draft`. Omit it or send null to act on the current draft under a row lock. These routes do not trim strings or convert \"\" to null, so an empty string is compared like any other value and never matches."}}}}}},"responses":{"200":{"description":"Published.","content":{"application/json":{"schema":{"type":"object","required":["status","published","template"],"properties":{"status":{"const":"success"},"published":{"$ref":"#/components/schemas/TemplateVersion"},"template":{"$ref":"#/components/schemas/TemplateStatus"}}}}}},"400":{"$ref":"#/components/responses/SiteRequired"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanUpgradeRequired"},"403":{"description":"Refused. Codes: `insufficient_scope` (the credential lacks design.read / design.write; content scopes do not imply them), `role_forbidden` (OAuth lane: the signed-in person's role on this help center cannot edit the design, e.g. a translator or viewer), `design_forbidden` (the `api.design` gate: the user behind the credential can no longer edit this help center's design in the dashboard), `site_forbidden` (OAuth lane: the named site is not part of the account). The form request's own refusal carries no code (\"You are not allowed to read/change this help center's design with this credential. ...\").","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/InsufficientScopeError"},{"$ref":"#/components/schemas/RoleForbiddenError"},{"$ref":"#/components/schemas/SiteForbiddenError"},{"$ref":"#/components/schemas/Error"},{"$ref":"#/components/schemas/TemplateDesignForbiddenError"}]}}}},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"`nothing_to_publish` (the draft is identical to the live design), `template_migration_required` (classic v1 site; carries `customizer_url`) or `templating_unavailable`.","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/TemplateNothingToPublishError"},{"$ref":"#/components/schemas/TemplateUnavailableError"},{"$ref":"#/components/schemas/TemplateMigrationRequiredError"}]}}}},"412":{"description":"`stale_draft`: an `etag` was sent and the draft has changed since; nothing was changed. The body carries the draft's current `etag`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TemplateStaleDraftError"}}}},"422":{"description":"Either the request failed validation (`status: validation_error`), or an AI agent's key would have added a CSS address outside the help center (`code: agent_css_url_forbidden`; nothing was changed).","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/ValidationError"},{"$ref":"#/components/schemas/TemplateCssUrlForbiddenError"}]}}}},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/v1/template/schema":{"get":{"operationId":"getTemplateSchema","summary":"Get the template customization guide","description":"A topic-by-topic guide to customizing the design, generated in part from the live component registry and settings schema: the component catalogue (types, pages, regions, nesting rules, settings), every path `set` accepts, the operation fields PATCH /v1/template/draft takes, and limits. It is the machine-readable reference for those, not a JSON Schema document. The `components` topic with no `types`, or with an unknown one, still answers 200 with a `message` and the component index.","tags":["Template"],"security":[{"apiKey":["design.read"]},{"oauth2":["design.read"]}],"parameters":[{"$ref":"#/components/parameters/XHCioSite"},{"$ref":"#/components/parameters/SiteQuery"},{"name":"topic","in":"query","required":false,"schema":{"type":["string","null"],"enum":["overview","components","settings","theme","css","scripts","navigation",null]},"description":"Guide topic. Omitted or empty means `overview`."},{"name":"types","in":"query","required":false,"schema":{"type":["string","null"],"maxLength":500},"description":"Comma-separated component types for the `components` topic; at most 8 are described in full. Ignored by other topics."}],"responses":{"200":{"description":"The guide for the requested topic.","content":{"application/json":{"schema":{"type":"object","required":["status","guide"],"properties":{"status":{"const":"success"},"guide":{"$ref":"#/components/schemas/TemplateGuide"}}}}}},"400":{"$ref":"#/components/responses/SiteRequired"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanUpgradeRequired"},"403":{"description":"Refused. Codes: `insufficient_scope` (the credential lacks design.read / design.write; content scopes do not imply them), `role_forbidden` (OAuth lane: the signed-in person's role on this help center cannot edit the design, e.g. a translator or viewer), `design_forbidden` (the `api.design` gate: the user behind the credential can no longer edit this help center's design in the dashboard), `site_forbidden` (OAuth lane: the named site is not part of the account). The form request's own refusal carries no code (\"You are not allowed to read/change this help center's design with this credential. ...\").","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/InsufficientScopeError"},{"$ref":"#/components/schemas/RoleForbiddenError"},{"$ref":"#/components/schemas/SiteForbiddenError"},{"$ref":"#/components/schemas/Error"},{"$ref":"#/components/schemas/TemplateDesignForbiddenError"}]}}}},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"The site cannot use the template API: `template_migration_required` (still on the classic v1 template; carries `customizer_url`, where the owner migrates it) or `templating_unavailable` (the v2 Template Editor is switched off on this environment).","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/TemplateUnavailableError"},{"$ref":"#/components/schemas/TemplateMigrationRequiredError"}]}}}},"422":{"$ref":"#/components/responses/ValidationFailed"},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/v1/template/themes":{"get":{"operationId":"listTemplateThemes","summary":"List the theme gallery","description":"Every starter theme, in gallery order, with `current` marking the one the draft was started from. If the site has no active draft, one is forked from the live version first.","tags":["Template"],"security":[{"apiKey":["design.read"]},{"oauth2":["design.read"]}],"parameters":[{"$ref":"#/components/parameters/XHCioSite"},{"$ref":"#/components/parameters/SiteQuery"}],"responses":{"200":{"description":"The theme gallery.","content":{"application/json":{"schema":{"type":"object","required":["status","themes"],"properties":{"status":{"const":"success"},"themes":{"type":"array","items":{"$ref":"#/components/schemas/TemplateThemeSummary"}}}}}}},"400":{"$ref":"#/components/responses/SiteRequired"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanUpgradeRequired"},"403":{"description":"Refused. Codes: `insufficient_scope` (the credential lacks design.read / design.write; content scopes do not imply them), `role_forbidden` (OAuth lane: the signed-in person's role on this help center cannot edit the design, e.g. a translator or viewer), `design_forbidden` (the `api.design` gate: the user behind the credential can no longer edit this help center's design in the dashboard), `site_forbidden` (OAuth lane: the named site is not part of the account). The form request's own refusal carries no code (\"You are not allowed to read/change this help center's design with this credential. ...\").","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/InsufficientScopeError"},{"$ref":"#/components/schemas/RoleForbiddenError"},{"$ref":"#/components/schemas/SiteForbiddenError"},{"$ref":"#/components/schemas/Error"},{"$ref":"#/components/schemas/TemplateDesignForbiddenError"}]}}}},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"The site cannot use the template API: `template_migration_required` (still on the classic v1 template; carries `customizer_url`, where the owner migrates it) or `templating_unavailable` (the v2 Template Editor is switched off on this environment).","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/TemplateUnavailableError"},{"$ref":"#/components/schemas/TemplateMigrationRequiredError"}]}}}},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/v1/template/themes/{theme}":{"get":{"operationId":"getTemplateTheme","summary":"Get one theme and what applying it would change","description":"The theme's gallery entry plus its description, screenshots and page anatomy (catalogue themes only), `changed_sections` (the sections applying it would change on the current draft) and `sections` (every section key). If the site has no active draft, one is forked from the live version first.","tags":["Template"],"security":[{"apiKey":["design.read"]},{"oauth2":["design.read"]}],"parameters":[{"$ref":"#/components/parameters/XHCioSite"},{"$ref":"#/components/parameters/SiteQuery"},{"name":"theme","in":"path","required":true,"schema":{"type":"string"},"description":"A theme key from `GET /v1/template/themes`. Any other value answers 404 `unknown_theme`."}],"responses":{"200":{"description":"The theme.","content":{"application/json":{"schema":{"type":"object","required":["status","theme"],"properties":{"status":{"const":"success"},"theme":{"$ref":"#/components/schemas/TemplateThemeDetail"}}}}}},"400":{"$ref":"#/components/responses/SiteRequired"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanUpgradeRequired"},"403":{"description":"Refused. Codes: `insufficient_scope` (the credential lacks design.read / design.write; content scopes do not imply them), `role_forbidden` (OAuth lane: the signed-in person's role on this help center cannot edit the design, e.g. a translator or viewer), `design_forbidden` (the `api.design` gate: the user behind the credential can no longer edit this help center's design in the dashboard), `site_forbidden` (OAuth lane: the named site is not part of the account). The form request's own refusal carries no code (\"You are not allowed to read/change this help center's design with this credential. ...\").","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/InsufficientScopeError"},{"$ref":"#/components/schemas/RoleForbiddenError"},{"$ref":"#/components/schemas/SiteForbiddenError"},{"$ref":"#/components/schemas/Error"},{"$ref":"#/components/schemas/TemplateDesignForbiddenError"}]}}}},"404":{"description":"`unknown_theme`, or the API key's site is gone (no code).","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/Error"},{"allOf":[{"$ref":"#/components/schemas/TemplateNotFoundError"},{"properties":{"code":{"enum":["unknown_theme"]}}}]}]}}}},"409":{"description":"The site cannot use the template API: `template_migration_required` (still on the classic v1 template; carries `customizer_url`, where the owner migrates it) or `templating_unavailable` (the v2 Template Editor is switched off on this environment).","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/TemplateUnavailableError"},{"$ref":"#/components/schemas/TemplateMigrationRequiredError"}]}}}},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/v1/template/themes/{theme}/apply":{"post":{"operationId":"applyTemplateTheme","summary":"Apply a theme's sections to the draft","description":"Copies the theme's sections onto the draft (every section by default) and records the theme as the draft's `preset_key`. Applying page sections replaces those pages' components (new component ids); applying `customCss` replaces the draft's custom CSS items; `warnings` says so. Draft only: nothing visitors see changes until publish. Writes also count against the tighter public-api-write limit (120 requests/minute per credential by default).","tags":["Template"],"security":[{"apiKey":["design.write"]},{"oauth2":["design.write"]}],"parameters":[{"$ref":"#/components/parameters/XHCioSite"},{"$ref":"#/components/parameters/SiteQuery"},{"name":"theme","in":"path","required":true,"schema":{"type":"string"},"description":"A theme key from `GET /v1/template/themes`. Any other value answers 404 `unknown_theme`."}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"sections":{"type":["array","null"],"minItems":1,"items":{"type":"string","enum":["brand","theme.colors","theme.darkColors","theme.fonts","theme.radius","theme.density","pages.home","pages.category","pages.article","pages.search","pages.contact","pages.notFound","globals.navigation","globals.scripts","meta","customCss"]},"description":"Sections to take from the theme. Omitted or null takes every section; an empty array is refused (422). Duplicates are ignored."},"etag":{"type":["string","null"],"maxLength":100,"description":"Optimistic lock: the draft etag from your last read (`template.draft.etag`, or `etag` from GET /v1/template/draft). When given and the draft has changed since, nothing is changed and the answer is 412 `stale_draft`. Omit it or send null to act on the current draft under a row lock. These routes do not trim strings or convert \"\" to null, so an empty string is compared like any other value and never matches."}}}}}},"responses":{"200":{"description":"Applied.","content":{"application/json":{"schema":{"type":"object","required":["status","applied","warnings","template"],"properties":{"status":{"const":"success"},"applied":{"type":"object","required":["theme","sections"],"properties":{"theme":{"type":"string"},"sections":{"type":"array","items":{"type":"string","enum":["brand","theme.colors","theme.darkColors","theme.fonts","theme.radius","theme.density","pages.home","pages.category","pages.article","pages.search","pages.contact","pages.notFound","globals.navigation","globals.scripts","meta","customCss"]}}}},"warnings":{"type":"array","items":{"type":"string","enum":["The theme's custom CSS replaced the draft's custom CSS items.","The theme's page layouts replaced the draft's components on the pages you applied; component ids changed. Read the template again before editing components."]}},"template":{"$ref":"#/components/schemas/TemplateStatus"}}}}}},"400":{"$ref":"#/components/responses/SiteRequired"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanUpgradeRequired"},"403":{"description":"Refused. Codes: `insufficient_scope` (the credential lacks design.read / design.write; content scopes do not imply them), `role_forbidden` (OAuth lane: the signed-in person's role on this help center cannot edit the design, e.g. a translator or viewer), `design_forbidden` (the `api.design` gate: the user behind the credential can no longer edit this help center's design in the dashboard), `site_forbidden` (OAuth lane: the named site is not part of the account). The form request's own refusal carries no code (\"You are not allowed to read/change this help center's design with this credential. ...\").","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/InsufficientScopeError"},{"$ref":"#/components/schemas/RoleForbiddenError"},{"$ref":"#/components/schemas/SiteForbiddenError"},{"$ref":"#/components/schemas/Error"},{"$ref":"#/components/schemas/TemplateDesignForbiddenError"}]}}}},"404":{"description":"`unknown_theme`, or the API key's site is gone (no code).","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/Error"},{"allOf":[{"$ref":"#/components/schemas/TemplateNotFoundError"},{"properties":{"code":{"enum":["unknown_theme"]}}}]}]}}}},"409":{"description":"The site cannot use the template API: `template_migration_required` (still on the classic v1 template; carries `customizer_url`, where the owner migrates it) or `templating_unavailable` (the v2 Template Editor is switched off on this environment).","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/TemplateUnavailableError"},{"$ref":"#/components/schemas/TemplateMigrationRequiredError"}]}}}},"412":{"description":"`stale_draft`: an `etag` was sent and the draft has changed since; nothing was changed. The body carries the draft's current `etag`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TemplateStaleDraftError"}}}},"422":{"description":"Either the request failed validation (`status: validation_error`), or an AI agent's key would have added a CSS address outside the help center (`code: agent_css_url_forbidden`; nothing was changed).","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/ValidationError"},{"$ref":"#/components/schemas/TemplateCssUrlForbiddenError"}]}}}},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/v1/template/versions":{"get":{"operationId":"listTemplateVersions","summary":"List published versions","description":"The publication history: published and archived versions, newest `published_at` first, at most 50. Not paginated.","tags":["Template"],"security":[{"apiKey":["design.read"]},{"oauth2":["design.read"]}],"parameters":[{"$ref":"#/components/parameters/XHCioSite"},{"$ref":"#/components/parameters/SiteQuery"}],"responses":{"200":{"description":"The versions.","content":{"application/json":{"schema":{"type":"object","required":["status","versions"],"properties":{"status":{"const":"success"},"versions":{"type":"array","maxItems":50,"items":{"$ref":"#/components/schemas/TemplateVersionListItem"}}}}}}},"400":{"$ref":"#/components/responses/SiteRequired"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanUpgradeRequired"},"403":{"description":"Refused. Codes: `insufficient_scope` (the credential lacks design.read / design.write; content scopes do not imply them), `role_forbidden` (OAuth lane: the signed-in person's role on this help center cannot edit the design, e.g. a translator or viewer), `design_forbidden` (the `api.design` gate: the user behind the credential can no longer edit this help center's design in the dashboard), `site_forbidden` (OAuth lane: the named site is not part of the account). The form request's own refusal carries no code (\"You are not allowed to read/change this help center's design with this credential. ...\").","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/InsufficientScopeError"},{"$ref":"#/components/schemas/RoleForbiddenError"},{"$ref":"#/components/schemas/SiteForbiddenError"},{"$ref":"#/components/schemas/Error"},{"$ref":"#/components/schemas/TemplateDesignForbiddenError"}]}}}},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"The site cannot use the template API: `template_migration_required` (still on the classic v1 template; carries `customizer_url`, where the owner migrates it) or `templating_unavailable` (the v2 Template Editor is switched off on this environment).","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/TemplateUnavailableError"},{"$ref":"#/components/schemas/TemplateMigrationRequiredError"}]}}}},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/v1/template/versions/{versionId}/restore":{"post":{"operationId":"restoreTemplateVersion","summary":"Load an earlier published version into the draft","description":"Copies a published or archived version's document, translations and preset key into the draft for review. Unlike the customizer's one-click revert, it does not go live until POST /v1/template/publish. Writes also count against the tighter public-api-write limit (120 requests/minute per credential by default).","tags":["Template"],"security":[{"apiKey":["design.write"]},{"oauth2":["design.write"]}],"parameters":[{"$ref":"#/components/parameters/XHCioSite"},{"$ref":"#/components/parameters/SiteQuery"},{"name":"versionId","in":"path","required":true,"schema":{"type":"integer","minimum":0},"description":"A version id from GET /v1/template/versions. The route does not constrain it; a value that is not all digits, or names no published/archived version of this help center, answers 404 `version_not_found`."}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"etag":{"type":["string","null"],"maxLength":100,"description":"Optimistic lock: the draft etag from your last read (`template.draft.etag`, or `etag` from GET /v1/template/draft). When given and the draft has changed since, nothing is changed and the answer is 412 `stale_draft`. Omit it or send null to act on the current draft under a row lock. These routes do not trim strings or convert \"\" to null, so an empty string is compared like any other value and never matches."}}}}}},"responses":{"200":{"description":"Restored into the draft.","content":{"application/json":{"schema":{"type":"object","required":["status","restored","template"],"properties":{"status":{"const":"success"},"restored":{"type":"integer","description":"The restored version's id."},"template":{"$ref":"#/components/schemas/TemplateStatus"}}}}}},"400":{"$ref":"#/components/responses/SiteRequired"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanUpgradeRequired"},"403":{"description":"Refused. Codes: `insufficient_scope` (the credential lacks design.read / design.write; content scopes do not imply them), `role_forbidden` (OAuth lane: the signed-in person's role on this help center cannot edit the design, e.g. a translator or viewer), `design_forbidden` (the `api.design` gate: the user behind the credential can no longer edit this help center's design in the dashboard), `site_forbidden` (OAuth lane: the named site is not part of the account). The form request's own refusal carries no code (\"You are not allowed to read/change this help center's design with this credential. ...\").","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/InsufficientScopeError"},{"$ref":"#/components/schemas/RoleForbiddenError"},{"$ref":"#/components/schemas/SiteForbiddenError"},{"$ref":"#/components/schemas/Error"},{"$ref":"#/components/schemas/TemplateDesignForbiddenError"}]}}}},"404":{"description":"`version_not_found`, or the API key's site is gone (no code).","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/Error"},{"allOf":[{"$ref":"#/components/schemas/TemplateNotFoundError"},{"properties":{"code":{"enum":["version_not_found"]}}}]}]}}}},"409":{"description":"The site cannot use the template API: `template_migration_required` (still on the classic v1 template; carries `customizer_url`, where the owner migrates it) or `templating_unavailable` (the v2 Template Editor is switched off on this environment).","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/TemplateUnavailableError"},{"$ref":"#/components/schemas/TemplateMigrationRequiredError"}]}}}},"412":{"description":"`stale_draft`: an `etag` was sent and the draft has changed since; nothing was changed. The body carries the draft's current `etag`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TemplateStaleDraftError"}}}},"422":{"description":"Either the request failed validation (`status: validation_error`), or an AI agent's key would have added a CSS address outside the help center (`code: agent_css_url_forbidden`; nothing was changed).","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/ValidationError"},{"$ref":"#/components/schemas/TemplateCssUrlForbiddenError"}]}}}},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/v1/agent/help-centers":{"post":{"operationId":"createAgentHelpCenter","summary":"Create an unclaimed help center, with no account","description":"Before submitting a request, read the [Terms of Service for API and agent use](https://helpcenter.io/legal/terms#agents). Act only with your user's authority. For an AI agent acting for a user who has no HelpCenter.io account yet. Send no credential. Creates a help center that nobody but its team can see until it is published (which only the user can do, after claiming it), not shown to search engines, on a temporary address, with nobody as its owner yet, and returns an agent key for it and the claim link to give the user. Fill it through the other /v1 routes with that key, then hand over the claim link: the user claims the help center by signing up or signing in with a verified email. An unclaimed help center expires 7 days after creation (`expires_at`) and is then subject to deletion. An open migration request pauses expiry; a new 7-day period starts when the request ends. Until it is claimed it answers only its content and design routes, and holds at most 500 articles and 100 categories and stores at most 500 MB of images and files (`limits`). Limited per client address to 1 creation an hour and 3 a day, failed requests not counted (an IPv6 address counts with the rest of its /64; an IPv4 address on its own), and to 200 unclaimed help centers created this way open at once (the ones migration requests create are counted apart); past either, file a migration request (`POST /v1/agent/migration-requests`). Requests are also throttled at 10 a minute per client address, failed ones included. HelpCenter.io's MCP connector sends every agent it serves from one address, so a creation it signs counts instead in its own allowance, 60 an hour and 300 open at once across all of those agents, and never against an address or the 200. The response is not cacheable (`Cache-Control: no-store`).","tags":["Agent help centers"],"security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string","maxLength":100,"description":"The help center's name, usually the user's product or company. A name containing a protected brand name is refused (`name_blocked`)."},"source_url":{"type":["string","null"],"format":"uri","maxLength":2048,"description":"The https address of the knowledge base being migrated, if any. Creating the help center reads nothing there; it is the source a migration request for this help center (`POST /v1/agent/help-centers/{id}/migration-request`) migrates when it names none."},"language":{"type":["string","null"],"default":"en","description":"The help center's default language, as a supported locale code (`en`, `de`, `zh-Hans`...). It starts with this one alone: an article or category in another is refused, and an import (`/v1/imports`) adds the languages it brings."}}}}}},"responses":{"201":{"description":"Created. `api_key` is shown only here.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentHelpCenterCreated"}}}},"404":{"description":"The feature is off here.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"`validation_error` (with `errors`), or `name_blocked`: the name contains a protected brand name.","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/ValidationError"},{"$ref":"#/components/schemas/Error"}]}}}},"429":{"description":"`limit_reached`: this client address created a help center in the last hour (`help_centers_per_hour`) or 3 in the last day (`help_centers_per_day`); or, with only a `message`, more than 10 requests in a minute. Wait `Retry-After` seconds, or file a migration request.","headers":{"Retry-After":{"description":"Seconds until another creation is allowed.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/LimitReachedError"},{"$ref":"#/components/schemas/RateLimitError"}]}}}},"503":{"description":"`capacity_reached`: 200 unclaimed help centers created this way are open, the most allowed at once; try later or file a migration request (`migration_request_endpoint`). `busy`: many are being created this moment; retry after `Retry-After` seconds.","headers":{"Retry-After":{"description":"With `busy` only: seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/agent/help-centers/{id}":{"get":{"operationId":"getAgentHelpCenter","summary":"Read an unclaimed help center's status","description":"With the agent key `POST /v1/agent/help-centers` returned, for that help center only: any other id, and any other credential (an ordinary API key or an OAuth token), answers 404 once past the checks every route makes. Whether it has been claimed, or its link is held for someone confirming their address and until when, when it expires, what it holds against its limits, the claim link again while it is unclaimed, a new preview link (`preview_url`) to check the help center as its readers will see it (after the claim, only while its owner may publish it), and what is left to set up: `setup` and its `next_step`, the user's step to pass on, and `capabilities`, what the key may do on it now. Not cacheable (`Cache-Control: no-store`).","tags":["Agent help centers"],"security":[{"apiKey":["content.read"]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The help center's `site_id`, as `POST /v1/agent/help-centers` returned it."}],"responses":{"200":{"description":"The help center's status.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentHelpCenterStatus"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanUpgradeRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"The help center is not this agent key's own, does not exist or has been deleted, or the feature is off here.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"410":{"$ref":"#/components/responses/AgentKeyExpired"},"429":{"$ref":"#/components/responses/TooManyRequests"}}},"delete":{"operationId":"discardAgentHelpCenter","summary":"Delete an unclaimed help center","description":"With the agent key `POST /v1/agent/help-centers` returned, for that help center only: any other id, and any other credential (an ordinary API key or an OAuth token), answers 404 once past the checks every route makes. Deletes it completely, with everything in it: articles, categories, images, its design and the agent key itself, which stops working at once. A claimed help center is its owner's and is refused (`already_claimed`).","tags":["Agent help centers"],"security":[{"apiKey":["content.write"]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The help center's `site_id`, as `POST /v1/agent/help-centers` returned it."}],"responses":{"200":{"description":"Deleted.","content":{"application/json":{"schema":{"type":"object","required":["status","site_id"],"properties":{"status":{"const":"success"},"site_id":{"type":"integer","description":"The deleted help center's id."}}}}}},"202":{"description":"Not yet fully deleted: a write to the help center that was already under way (an upload, an import, an article being saved) is still running, and the deletion waits for it before deleting what it wrote; or a step failed or has not finished, typically one outside the database (stored images, the search index, which the search engine deletes in the background, or the CDN). The deletion is retried automatically; the agent need not call again.","content":{"application/json":{"schema":{"type":"object","required":["status","site_id","message"],"properties":{"status":{"const":"pending"},"site_id":{"type":"integer"},"message":{"type":"string"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanUpgradeRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"The help center is not this agent key's own, does not exist or has been deleted, or the feature is off here.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`already_claimed`: a person has claimed this help center. `migration_in_progress`: it is in an open migration request. Nothing was deleted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"410":{"$ref":"#/components/responses/AgentKeyExpired"},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/v1/agent/help-centers/{id}/migration-request":{"post":{"operationId":"createAgentHelpCenterMigrationRequest","summary":"File a migration request for an unclaimed help center","description":"With the agent key `POST /v1/agent/help-centers` returned, for that help center only: any other id, and any other credential (an ordinary API key or an OAuth token), answers 404 once past the checks every route makes. The same as `POST /v1/agent/migration-requests` with this help center as its one source, and the same response; the source's `url` defaults to the `source_url` the help center was created with. HelpCenter.io's migration team moves the knowledge bases for the agent's user. A source the importer reads by itself, a public Zendesk, Intercom, Document360, Help Scout, Front or HelpDocs help center, is migrated automatically; anything else goes to our team (`with_our_team`). Follow both with `GET /v1/agent/migration-requests/{id}`. If you already wrote articles from a source into its help center, give each the `external_id` of its address at that source: while the help center is unclaimed, the migration replaces those in place from the source, and keeps every other article you wrote as it is. While a request is open, an agent key's writes to its help centers answer 409 `migration_in_progress` (reads still work), and their 7-day deletion clock is paused; when it ends, an unclaimed one is deleted 7 days later. Each help center sits in one open request at a time. Nothing is ever sent to the user's address automatically, not even on completion: tell the user yourself, and hand them `handoff_url`. Counts against the same 3 requests a day per client address, or, signed by HelpCenter.io's MCP connector, against its 30 a day.","tags":["Agent help centers"],"security":[{"apiKey":["content.write"]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The help center's `site_id`, as `POST /v1/agent/help-centers` returned it."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email"],"properties":{"email":{"type":"string","format":"email","maxLength":255,"description":"The user's address, for our team only. Nothing is sent to it automatically. How HelpCenter.io handles it: [Privacy Policy](https://helpcenter.io/legal/privacy#agent-migrations)."},"url":{"type":["string","null"],"format":"uri","maxLength":2048,"description":"The https address of the knowledge base to migrate. Required when the help center was created without a `source_url`."},"name":{"type":["string","null"],"maxLength":100},"article_count":{"type":["integer","null"],"minimum":0,"maximum":1000000},"note":{"type":["string","null"],"maxLength":2000}}}}}},"responses":{"201":{"description":"Filed. `status_token` and `handoff_url` are shown only here. Our team is told of it; nothing is ever sent to the user's address automatically.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentMigrationRequestCreated"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanUpgradeRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"The help center is not this agent key's own, does not exist or has been deleted, or the feature is off here.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`migration_in_progress`: the help center is already in an open migration request. `already_claimed`: a person has claimed it, so it is theirs. Nothing was filed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"410":{"$ref":"#/components/responses/AgentKeyExpired"},"422":{"description":"`validation_error`, with `errors`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}}},"429":{"description":"`limit_reached` (`migration_requests_per_day`): this client address filed 3 requests in the last day; or, with only a `message`, the write rate limit. Wait `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until another request is allowed.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/MigrationRequestLimitError"},{"$ref":"#/components/schemas/RateLimitError"}]}}}}}}},"/v1/agent/migration-requests":{"post":{"operationId":"createAgentMigrationRequest","summary":"File a migration request for up to 50 help centers, with no account","description":"Before submitting a request, read the [Terms of Service for API and agent use](https://helpcenter.io/legal/terms#agents). Act only with your user's authority, and send their email address only with their permission. Send no credential. HelpCenter.io's migration team moves the knowledge bases for the agent's user. A source the importer reads by itself, a public Zendesk, Intercom, Document360, Help Scout, Front or HelpDocs help center, is migrated automatically; anything else goes to our team (`with_our_team`). Follow both with `GET /v1/agent/migration-requests/{id}`. If you already wrote articles from a source into its help center, give each the `external_id` of its address at that source: while the help center is unclaimed, the migration replaces those in place from the source, and keeps every other article you wrote as it is. While a request is open, an agent key's writes to its help centers answer 409 `migration_in_progress` (reads still work), and their 7-day deletion clock is paused; when it ends, an unclaimed one is deleted 7 days later. Each help center sits in one open request at a time. The new help centers requests create count only against their own pool, at most 500 held by open requests at once, never against the 200 of `POST /v1/agent/help-centers`. Nothing is ever sent to the user's address automatically, not even on completion: tell the user yourself, and hand them `handoff_url`. Limited per client address to 3 requests a day, failed requests not counted (an IPv6 address counts with the rest of its /64; an IPv4 address on its own); also throttled at 20 a minute per client address, failed ones included. A request HelpCenter.io's MCP connector signs for the agents it serves counts instead against its 30 a day across all of them, and never against an address. The response is not cacheable (`Cache-Control: no-store`).","tags":["Agent help centers"],"security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email","sources"],"properties":{"email":{"type":"string","format":"email","maxLength":255,"description":"The user's address, for our team only: a person on our team may write to it about the migration. Nothing is sent to it automatically. How HelpCenter.io handles it: [Privacy Policy](https://helpcenter.io/legal/privacy#agent-migrations)."},"sources":{"type":"array","minItems":1,"maxItems":50,"items":{"$ref":"#/components/schemas/AgentMigrationSource"},"description":"The knowledge bases to migrate, one help center each. Name each one once: a source that repeats an earlier one, compared ignoring letter case in the scheme and host, a leading `www.`, the port, a trailing slash, the query and the fragment, is refused with 422 `validation_error`, its `errors` keyed `sources.<index>.url` and naming the source it repeats."}}}}}},"responses":{"201":{"description":"Filed. `status_token` and `handoff_url` are shown only here. Our team is told of it; nothing is ever sent to the user's address automatically.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentMigrationRequestCreated"}}}},"404":{"description":"The feature is off here.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`source_unavailable`: a help center a source's `agent_key` named was claimed, expired, deleted or put in another open request while this one was being filed. Nothing was filed; `errors` names the source.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"`validation_error`, with `errors`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}}},"429":{"description":"`limit_reached` (`migration_requests_per_day`): this client address filed 3 requests in the last day, failed ones not counted; or, with only a `message`, more than 20 requests in a minute. Wait `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until another request is allowed.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/MigrationRequestLimitError"},{"$ref":"#/components/schemas/RateLimitError"}]}}}},"503":{"description":"`capacity_reached`: the new help centers this request needs would pass the 500 that open migration requests may hold at once, so the whole request is refused; try later, or with fewer sources that need a new help center. `busy`: many are being created this moment; retry after `Retry-After` seconds. Nothing was filed.","headers":{"Retry-After":{"description":"With `busy` only: seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/agent/migration-requests/{id}":{"get":{"operationId":"getAgentMigrationRequest","summary":"Follow a migration request with its status token","description":"Send the request's `status_token` as `Authorization: Bearer <status_token>`; no agent key, API key or OAuth token reads it. Works until 30 days after the request ends, also after its help centers are claimed. Any mismatch, a wrong token or id included, answers the same 404. At most 60 checks an hour per request; every 900 seconds (`retry_after`) is enough. Not cacheable.","tags":["Agent help centers"],"security":[{"migrationStatusToken":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The `request_id` the request was filed with."}],"responses":{"200":{"description":"Where the request stands.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentMigrationRequestStatus"}}}},"404":{"description":"No such request, a missing or wrong status token, one past its 30 days after the request ended, or the feature is off here.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"`limit_reached`: 60 checks of this request in the last hour; or, with only a `message`, more than 120 requests in a minute from this client address. Wait `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until the next check is allowed.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/Error"},{"$ref":"#/components/schemas/RateLimitError"}]}}}}}}},"/v1/custom-domain":{"get":{"operationId":"getCustomDomain","summary":"Read the custom domain's status","description":"The domain, the DNS record the user must add, and where it stands on the way to `live`. Poll it while the user sets up DNS: `status` moves from `pending_dns` through `verifying` and `issuing_certificate` to `live`. HelpCenter.io's checks run every 5 minutes and move a domain one step at a time, so it is usually live 10 to 20 minutes after the record appears, and later when the record comes more than a day after the domain was set, from when they run hourly. An API key needs `domain.manage`, which it holds only when it is a write key that one of the help center's Owners or Admins granted \"Allow managing the custom domain\" in Settings; without it the call answers `403 insufficient_scope`, naming that setting. An AI agent's key may call it once its help center is claimed and its owner has verified their email address and activated a plan; until then it answers `403 capability_locked` with the `next_step` its status shows.","tags":["Custom domain"],"security":[{"apiKey":["domain.manage"]},{"oauth2":["domain.manage"]}],"responses":{"200":{"description":"The custom domain's status.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomDomainResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanUpgradeRequired"},"400":{"$ref":"#/components/responses/SiteRequired"},"403":{"description":"`capability_locked`: an AI agent's key, whose help center is not claimed yet, or whose owner has not verified their email address or activated a plan; `next_step` says what is left. `insufficient_scope`: an API key not granted \"Allow managing the custom domain\", or an OAuth token without `domain.manage`. `role_forbidden` (an OAuth token whose person is not an Owner or Admin of the help center), `site_forbidden` and `agent_not_allowed` (an unclaimed help center) as on every route.","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/CapabilityLockedError"},{"$ref":"#/components/schemas/InsufficientScopeError"},{"$ref":"#/components/schemas/RoleForbiddenError"},{"$ref":"#/components/schemas/SiteForbiddenError"},{"$ref":"#/components/schemas/AgentNotAllowedError"},{"$ref":"#/components/schemas/Error"}]}}}},"404":{"description":"The API key's help center has been deleted, or the feature is off here.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"410":{"$ref":"#/components/responses/AgentKeyExpired"},"429":{"$ref":"#/components/responses/TooManyRequests"}}},"put":{"operationId":"setCustomDomain","summary":"Set the custom domain","description":"Connects the help center to `domain`, as its Settings page does, and requests its certificate once the DNS record points here. A domain is live only once its DNS points at the help center and its certificate is issued; nothing here shortens that. Changing to a new domain counts against the help center's 4 changes in a rolling month (the month up to now, not a calendar month) and stops the old domain answering; setting the domain it already has changes nothing. An API key needs `domain.manage`, which it holds only when it is a write key that one of the help center's Owners or Admins granted \"Allow managing the custom domain\" in Settings; without it the call answers `403 insufficient_scope`, naming that setting. An AI agent's key may call it once its help center is claimed and its owner has verified their email address and activated a plan; until then it answers `403 capability_locked` with the `next_step` its status shows.","tags":["Custom domain"],"security":[{"apiKey":["domain.manage"]},{"oauth2":["domain.manage"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["domain"],"properties":{"domain":{"type":"string","maxLength":253,"description":"A host name such as `help.example.com`, without `http(s)://` or a path. Leading `www.` labels are dropped, every consecutive one and in any letter case (`www.help.example.com` and `www.www.help.example.com` are both stored as `help.example.com`); a `www.` after any other label is kept (`docs.www.example.com` is stored unchanged). Not a `helpcenter.io` address.","examples":["help.example.com"]}}}}}},"responses":{"200":{"description":"Set, or already set to this domain (nothing changed then). The status is usually `pending_dns` until the user adds the record in `dns_records`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomDomainResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanUpgradeRequired"},"400":{"$ref":"#/components/responses/SiteRequired"},"403":{"description":"`capability_locked`: an AI agent's key, whose help center is not claimed yet, or whose owner has not verified their email address or activated a plan; `next_step` says what is left. `custom_domain_not_in_plan`: the help center's plan does not include a custom domain. `insufficient_scope`: an API key not granted \"Allow managing the custom domain\", or an OAuth token without `domain.manage`. `role_forbidden` (an OAuth token whose person is not an Owner or Admin of the help center), `site_forbidden` and `agent_not_allowed` (an unclaimed help center) as on every route.","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/CapabilityLockedError"},{"$ref":"#/components/schemas/CustomDomainNotInPlanError"},{"$ref":"#/components/schemas/InsufficientScopeError"},{"$ref":"#/components/schemas/RoleForbiddenError"},{"$ref":"#/components/schemas/SiteForbiddenError"},{"$ref":"#/components/schemas/AgentNotAllowedError"},{"$ref":"#/components/schemas/Error"}]}}}},"404":{"description":"The API key's help center has been deleted, or the feature is off here.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`domain_taken`: another help center already holds the domain, as its custom domain or an alias; the message names no other help center. `custom_domain_busy`: another change to this help center's custom domain, or another help center's save of the same domain, is still running; retry in a few seconds. `migration_in_progress`: an AI agent's key, while a migration request for its help center is open. Nothing was changed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"410":{"$ref":"#/components/responses/AgentKeyExpired"},"422":{"$ref":"#/components/responses/ValidationFailed"},"429":{"description":"`custom_domain_limit_reached`: the help center has made 4 custom domain changes in the last month, each of which requests a certificate; nothing was changed, and `Retry-After` says when the next one is allowed. Or an API-wide rate limit, as on every route.","headers":{"Retry-After":{"description":"Seconds until the next change is allowed.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/CustomDomainLimitReachedError"},{"$ref":"#/components/schemas/RateLimitError"}]}}}}}},"delete":{"operationId":"removeCustomDomain","summary":"Remove the custom domain","description":"Disconnects the help center from its custom domain, as its Settings page does. An API key needs `domain.manage`, which it holds only when it is a write key that one of the help center's Owners or Admins granted \"Allow managing the custom domain\" in Settings; without it the call answers `403 insufficient_scope`, naming that setting. An AI agent's key may call it once its help center is claimed and its owner has verified their email address and activated a plan; until then it answers `403 capability_locked` with the `next_step` its status shows.","tags":["Custom domain"],"security":[{"apiKey":["domain.manage"]},{"oauth2":["domain.manage"]}],"responses":{"200":{"description":"Removed, or there was none to remove. `custom_domain.domain` is null. The help center answers on its own address again; its certificate for the old domain is deleted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomDomainResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanUpgradeRequired"},"400":{"$ref":"#/components/responses/SiteRequired"},"403":{"description":"`capability_locked`: an AI agent's key, whose help center is not claimed yet, or whose owner has not verified their email address or activated a plan; `next_step` says what is left. `insufficient_scope`: an API key not granted \"Allow managing the custom domain\", or an OAuth token without `domain.manage`. `role_forbidden` (an OAuth token whose person is not an Owner or Admin of the help center), `site_forbidden` and `agent_not_allowed` (an unclaimed help center) as on every route.","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/CapabilityLockedError"},{"$ref":"#/components/schemas/InsufficientScopeError"},{"$ref":"#/components/schemas/RoleForbiddenError"},{"$ref":"#/components/schemas/SiteForbiddenError"},{"$ref":"#/components/schemas/AgentNotAllowedError"},{"$ref":"#/components/schemas/Error"}]}}}},"404":{"description":"The API key's help center has been deleted, or the feature is off here.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`custom_domain_busy`: another change to this help center's custom domain is still running; retry in a few seconds. `migration_in_progress`: an AI agent's key, while a migration request for its help center is open. Nothing was changed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"410":{"$ref":"#/components/responses/AgentKeyExpired"},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/v1/imports":{"get":{"operationId":"listContentImports","summary":"List the help center's imports","description":"This help center's imports, newest first (at most 50), each as `GET /v1/imports/{importId}` answers it. One a person set up in the dashboard is listed too, so an AI agent can follow and run a source its user connected there (a private Zendesk help center, or a HelpDocs site its pages do not show). Needs `content.write`, as every `/v1/imports` route does: an import writes into the help center. Not cacheable (`Cache-Control: no-store`).","tags":["Imports"],"security":[{"apiKey":["content.write"]},{"oauth2":["content.write"]}],"responses":{"200":{"description":"The imports.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContentImportList"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanUpgradeRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"The importer is off here.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"410":{"$ref":"#/components/responses/AgentKeyExpired"},"429":{"$ref":"#/components/responses/TooManyRequests"}}},"post":{"operationId":"scanContentImport","summary":"Scan a knowledge base to import","description":"Reads the help center at `url` (Zendesk Guide, Intercom, Document360, Help Scout Docs, Front Knowledge Base or HelpDocs) and answers its plan: categories, articles and languages, and where each language lands here. Every category and every language that lands here is chosen at first; nothing is imported until `POST /v1/imports/{importId}/run`. The scan reads the remote help center within this request (at most 150 requests and 40 seconds, honouring its robots.txt), so allow it time. An address is read as one of those platforms only when it is under the platform's own domain or its DNS points there with a CNAME, as a custom domain set up with the platform does; any other address is `other`, whatever its pages look like. One site is read for at most two scans or runs at once, and answers at most 20,000 of the importer's requests a day, across every help center importing from it: past either a scan answers `422` `host_busy` or `host_daily_limit`, and a run waits, `queued`, until there is room. Scanning again refreshes an import that has not run; an import of the same help center that is set up, running or done comes back as it is (`200`). Scanning another address cancels an earlier import that was scanned and never run.\n\nAn AI agent's key reads public sources only and sends no credentials (`422 credentials_not_accepted`): its user connects a private source from the dashboard after claiming the help center, and the agent can then follow and run that import here. For an agent a HelpDocs site is read from its public pages: its main language, and what anonymous readers see. One that asks readers to sign in, or whose custom HTML template lays its pages out its own way, answers `422 needs_credentials`: it is read with the account's API key. While its help center is unclaimed the agent does not confirm ownership: the person who claims it does, on the claim page, which names every source an agent imported. Every other caller sends `confirm_ownership: true`, the same agent key included once its help center is claimed.","tags":["Imports"],"security":[{"apiKey":["content.write"]},{"oauth2":["content.write"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["url"],"properties":{"url":{"type":"string","maxLength":2048,"description":"The address of the help center to import (a scheme is optional)."},"confirm_ownership":{"type":"boolean","description":"`true`: your user owns this help center or may copy its content. Required, except from an AI agent's key on its unclaimed help center."},"zendesk_email":{"type":["string","null"],"format":"email","maxLength":255,"description":"A Zendesk agent's email, with `zendesk_token`, for a Zendesk help center that needs signing in. Never from an AI agent's key."},"zendesk_token":{"type":["string","null"],"maxLength":200,"description":"That agent's Zendesk API token. Kept encrypted and never shown again. Never from an AI agent's key."},"helpdocs_api_key":{"type":["string","null"],"maxLength":200,"description":"The HelpDocs account's API key, which a HelpDocs import needs (an AI agent's reads the site's public pages instead). Kept encrypted and never shown again. Never from an AI agent's key."}}}}}},"responses":{"200":{"description":"An import of this help center that already existed: refreshed if it had not run, otherwise as it is.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContentImportResponse"}}}},"201":{"description":"Scanned: a new import, with its plan. An address the importer does not read by itself is saved too, with `platform` `other`, `importable: false` and `plan: null`: nothing in it can be chosen or run, so file a migration request for it instead.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContentImportResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanUpgradeRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"The importer is off here.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`migration_in_progress`: an open migration request holds this help center, so an AI agent's key writes nothing to it.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"410":{"$ref":"#/components/responses/AgentKeyExpired"},"422":{"description":"`validation_error`, with `errors`. `credentials_not_accepted`: an AI agent's key sent credentials. Otherwise the scan could not read the address, with `code` saying why (`needs_credentials` with the `platform` whose credentials it needs, `ip_in_blocked_range`, `robots` and the like; `ContentImportScanError` lists them) and a `message` to act on.","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/ValidationError"},{"$ref":"#/components/schemas/ContentImportScanError"}]}}}},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/v1/imports/{importId}":{"get":{"operationId":"getContentImport","summary":"Follow an import","description":"The plan, what is chosen, and how a run is going: `progress` (articles created, updated, conflicts with edits made here, images copied and failed), `imported_articles`, the imported articles waiting for review (`needs_review`), and `error` once a run stops. `limit` is what an AI agent's import into an unclaimed help center has room for. Not cacheable (`Cache-Control: no-store`).","tags":["Imports"],"security":[{"apiKey":["content.write"]},{"oauth2":["content.write"]}],"parameters":[{"name":"importId","in":"path","required":true,"schema":{"type":"integer"},"description":"The import's `id`, as `POST /v1/imports` or `GET /v1/imports` returned it."}],"responses":{"200":{"description":"The import.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContentImportResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanUpgradeRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"No import with this id on this help center (another help center's import answers the same), or the importer is off here.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"410":{"$ref":"#/components/responses/AgentKeyExpired"},"429":{"$ref":"#/components/responses/TooManyRequests"}}},"put":{"operationId":"chooseContentImport","summary":"Choose what an import brings","description":"The categories and languages to import, from the import's own plan, and whether articles arrive as drafts (`draft`, the default) or as they are at the source (`as_is`). Starts nothing, and never changes when an import runs or syncs. While an AI agent's help center is unclaimed, a choice that would take it past its 500 articles (drafts and Trash included) is refused with `422 limit_reached`, with the room left; choose fewer categories until it fits, or file a migration request.","tags":["Imports"],"security":[{"apiKey":["content.write"]},{"oauth2":["content.write"]}],"parameters":[{"name":"importId","in":"path","required":true,"schema":{"type":"integer"},"description":"The import's `id`, as `POST /v1/imports` or `GET /v1/imports` returned it."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["category_ids","languages"],"properties":{"category_ids":{"type":"array","minItems":1,"items":{"type":"string"},"description":"Ids from `plan.categories`: each brings everything nested under it."},"languages":{"type":"array","minItems":1,"items":{"type":"string"},"description":"Codes from `plan.languages` whose `lands_in` is not null."},"arrive_as":{"type":"string","enum":["draft","as_is"],"description":"Kept as it is when left out."}}}}}},"responses":{"200":{"description":"Chosen.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContentImportResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanUpgradeRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"No import with this id on this help center (another help center's import answers the same), or the importer is off here.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`import_running`: a run is under way. `import_cancelled`: the import was cancelled; scan its address again. `migration_in_progress`: an open migration request holds the help center.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"410":{"$ref":"#/components/responses/AgentKeyExpired"},"422":{"description":"`validation_error`, with `errors` (always, for an import that is not `importable`: it has no category to choose). `limit_reached`: the choice does not fit in the unclaimed help center. Nothing was saved.","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/ValidationError"},{"$ref":"#/components/schemas/ContentImportLimitReachedError"}]}}}},"429":{"$ref":"#/components/responses/TooManyRequests"}}},"delete":{"operationId":"cancelContentImport","summary":"Cancel an import","description":"As the dashboard cancels one: it never runs again, stops syncing, and its credentials are deleted. Nothing it already brought in is deleted, and scanning its address again later picks up what it brought, so nothing arrives twice. One queued or running cannot be cancelled (`409 import_running`): wait for the run to end, then cancel it.","tags":["Imports"],"security":[{"apiKey":["content.write"]},{"oauth2":["content.write"]}],"parameters":[{"name":"importId","in":"path","required":true,"schema":{"type":"integer"},"description":"The import's `id`, as `POST /v1/imports` or `GET /v1/imports` returned it."}],"responses":{"200":{"description":"Cancelled.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContentImportResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanUpgradeRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"No import with this id on this help center (another help center's import answers the same), or the importer is off here.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`import_running`: a run is under way. `migration_in_progress`: an open migration request holds the help center.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"410":{"$ref":"#/components/responses/AgentKeyExpired"},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/v1/imports/{importId}/run":{"post":{"operationId":"runContentImport","summary":"Run an import","description":"Starts the import now, with what is chosen (everything its plan found, until `PUT` chooses less), and runs a finished or failed one again: an article brought before is updated, never added twice, and one edited here since is kept and flagged for review. The run reads the source in the background; follow it with `GET /v1/imports/{importId}` until `status` is `completed` or `failed`. While an AI agent's help center is unclaimed, a run that would take it past its 500 articles is refused with `422 limit_reached`, and once it starts, the room it needs is its own. A run that finds more articles at the source than its scan did stops at the limit, keeping what arrived (`error.reason` `limit_reached`). Its images and files count toward the 500 MB the help center may store while unclaimed, and a run that would pass it stops at the image or file that does not fit, keeping what arrived (`error.reason` `limit_reached`, its `message` naming `storage_bytes`).","tags":["Imports"],"security":[{"apiKey":["content.write"]},{"oauth2":["content.write"]}],"parameters":[{"name":"importId","in":"path","required":true,"schema":{"type":"integer"},"description":"The import's `id`, as `POST /v1/imports` or `GET /v1/imports` returned it."}],"responses":{"202":{"description":"Queued.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContentImportResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanUpgradeRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"No import with this id on this help center (another help center's import answers the same), or the importer is off here.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`import_running`: a run is under way. `import_cancelled`: the import was cancelled; scan its address again. `not_importable`: the importer cannot read this source. `main_language_changed`: the import wrote into another main language, so a run would write nothing. `migration_in_progress`: an open migration request holds the help center.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"410":{"$ref":"#/components/responses/AgentKeyExpired"},"422":{"description":"`limit_reached`: the import as chosen does not fit in the unclaimed help center. Nothing was started.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContentImportLimitReachedError"}}}},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/v1/imports/{importId}/redirect-map":{"get":{"operationId":"getContentImportRedirectMap","summary":"Get an import's redirect map","description":"For every article and category the import brought in, the path its page had on the old help center (relative to that host, each language's own, like `/hc/en-us/articles/360001234-Reset-your-password`) and the path of the page that replaced it here, in that language: `/{lang}/content/{slug}` and `/{lang}/category/{id}-{name}` once the help center has more than one language, `/content/{slug}` and `/category/{id}-{name}` while it has one. Category and section pages are included where the source has pages of its own for them (Zendesk, Intercom, Front, Help Scout, HelpDocs; a Document360 Page category arrives as an article and is mapped as one). Each page holds up to 200, in the exact shape `POST /v1/redirects/bulk` takes: post `{\"redirects\": [...]}` as it is, page after page, once the old help center's address is about to point here. It reflects the help center as it is now: an article or category deleted here since is left out, and a page's current address is used. Every `from` and `to` is within the bulk endpoint's 191 characters (a `from` counted once percent-encoded): a longer category address here is given without its name (`/category/{id}`, the same page), and a longer old page path by its id alone (`…/sections/{id}` for `…/sections/{id}-{title}`; Zendesk's, Front's and Intercom's `articles`, `categories`, `sections` and `collections`, Help Scout's and HelpDocs' `article`, `category` and `collection`), which forwards every title after that id. Every entry is one the bulk endpoint saves and its old path then reaches: an old `/category/{number}-{name}` forwards even when a category here has that number, as long as the name is not one of that category's own. What the map cannot give that way is listed in `left_out`, with the old address and why, never dropped in silence: `source_path_too_long` (an old path with no id to give it by, too long even so), `target_path_too_long` (an article whose address here is too long: give it a shorter slug) and `source_answered_here` (an old path that is the address of a page here, which keeps showing that page: a category's own number with its own name, or a long one's number alone). Tell your user which old links will not forward. It does not check that each page is live; an AI agent's key may only redirect to a published, public page, so the bulk endpoint answers `target_not_found` for a draft until it is published (apply the map again then: an existing `from` is re-pointed, never duplicated, so fetch the map again first; a stale copy would point back a redirect someone has changed since). Reading it changes nothing, and needs no unlocked setup. Not cacheable (`Cache-Control: no-store`).","tags":["Imports"],"security":[{"apiKey":["content.write"]},{"oauth2":["content.write"]}],"parameters":[{"name":"importId","in":"path","required":true,"schema":{"type":"integer"},"description":"The import's `id`, as `POST /v1/imports` or `GET /v1/imports` returned it."},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":1},"description":"1-based page number. A page past the last answers an empty `redirects`."}],"responses":{"200":{"description":"A page of the map.","content":{"application/json":{"schema":{"type":"object","required":["status","import_id","source_host","redirects","left_out","meta"],"properties":{"status":{"const":"success"},"import_id":{"type":"integer"},"source_host":{"type":"string","description":"The old help center's host, the one every `from` is a path of."},"redirects":{"type":"array","maxItems":200,"items":{"$ref":"#/components/schemas/RedirectInput"},"description":"`from` is the old page's path, `to` the path of its page here. One entry per old path, in the order the import mapped them."},"left_out":{"type":"array","description":"Every old address the map could not give as a redirect, on every page whole, in the order the import mapped them. Empty when every old page forwards.","items":{"type":"object","required":["source_url","reason"],"properties":{"source_url":{"type":"string","description":"The old page's address, as the import read it."},"reason":{"type":"string","enum":["source_path_too_long","target_path_too_long","source_answered_here"]}}}},"meta":{"type":"object","required":["page","per_page","total_pages","items_count"],"properties":{"page":{"type":"integer"},"per_page":{"type":"integer","const":200,"description":"As many as one `POST /v1/redirects/bulk` takes."},"total_pages":{"type":"integer","minimum":1,"description":"At least 1, even when the map is empty."},"items_count":{"type":"integer","description":"Entries across all pages."}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanUpgradeRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"No import with this id on this help center (another help center's import answers the same), or the importer is off here.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"410":{"$ref":"#/components/responses/AgentKeyExpired"},"422":{"$ref":"#/components/responses/ValidationFailed"},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/v1/redirects":{"get":{"operationId":"listRedirects","summary":"List redirects","description":"The help center's redirects, newest first: each sends a request for a path the help center would otherwise answer with its not-found page on to another address, with a 301. These are the redirects Settings > Redirects lists; the ones a renamed article leaves behind are not among them. An AI agent's key may use this once its help center has unlocked redirects (its status's `capabilities.redirects`). Takes an API key of the help center: an OAuth token is refused with `api_key_required`.","tags":["Redirects"],"security":[{"apiKey":["content.read"]}],"parameters":[{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":1},"description":"1-based page number."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100,"default":50},"description":"Redirects per page."},{"name":"search","in":"query","required":false,"schema":{"type":"string","maxLength":255},"description":"Only redirects whose `from` or `to` contains this text. A leading `/` is ignored."}],"responses":{"200":{"description":"A page of redirects.","content":{"application/json":{"schema":{"type":"object","required":["status","redirects","meta"],"properties":{"status":{"const":"success"},"redirects":{"type":"array","items":{"$ref":"#/components/schemas/Redirect"}},"meta":{"type":"object","required":["page","per_page","total_pages","items_count"],"properties":{"page":{"type":"integer"},"per_page":{"type":"integer","description":"The effective `limit`."},"total_pages":{"type":"integer","minimum":1,"description":"At least 1, even when there are no redirects."},"items_count":{"type":"integer","description":"Redirects matching the query, across all pages."}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanUpgradeRequired"},"403":{"description":"`insufficient_scope`: the key lacks the scope (a read key writes). `agent_not_allowed`: the help center is unclaimed and this is not its AI agent's key. `capability_locked`: an AI agent's key whose help center has not unlocked redirects yet; `requires` lists what is left and `next_step` what its user does next (the claim link while unclaimed). `api_key_required`: an OAuth token; redirects take an API key of the help center.","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/InsufficientScopeError"},{"$ref":"#/components/schemas/AgentNotAllowedError"},{"$ref":"#/components/schemas/CapabilityLockedError"},{"$ref":"#/components/schemas/ApiKeyRequiredError"}]}}}},"404":{"description":"The feature is off here, which every redirects route answers whatever the key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"410":{"$ref":"#/components/responses/AgentKeyExpired"},"422":{"$ref":"#/components/responses/ValidationFailed"},"429":{"$ref":"#/components/responses/TooManyRequests"}}},"post":{"operationId":"createRedirect","summary":"Create or re-point a redirect","description":"Sends a path of the help center (`from`) to `to` with a 301, for as long as nothing else on the help center answers that path: an article, category or page there always wins. A path that already has a redirect is re-pointed (`action: updated`, 200) rather than duplicated. Paths are stored as the help center matches them: without the leading slash, percent-encoded as a browser sends them, and `from` is answered as the path below a subfolder address. A `to` on the help center itself (a path, or a URL on its subdomain, custom domain or subfolder address) is stored as its path, so the redirect follows the help center to a new address. Refused when no visitor's request for `from` could ever reach a redirect (`source_not_redirectable`): the help center reads a path's first segment as a language first, so a one-segment path like `/pricing` always ends on its home page, as does a path like `/xx/content/...` whose first segment is not one of its languages, and an article address in the form its languages do not use (`/en/content/...` while it has one language, `/content/...` once it has more) moves to the other form first; and an address the help center answers itself, such as `/categories`, `/contacts`, `/search` or an article or category that exists (an article address is free only while no article, renamed article's old slug or v1 slug match answers it; a category's address is its own with its number alone or one of its own names, and `/category/{number}-{another-name}` is free, so another site's old category address forwards). Refused too when it would send visitors in a loop (`redirect_loop`) or through more than 3 redirects in a row (`too_many_hops`) on any chain it is part of, the redirects already leading into it included.\n\nHelpCenter.io's policy: an API key's redirects stay on its help center unless it is a write key that one of the help center's Owners or Admins granted \"Allow redirects to other sites\" in Settings > API keys; without that, a `to` on another site answers `external_target_forbidden`. The dashboard's Settings > Redirects may point anywhere. A `to` on the help center must be the current address of a page of it that exists now, whatever the key (`target_not_found` otherwise): its home page, category list or contact page, a category a visitor may see, or a published, public article in one of its languages. An AI agent's key is never granted it, so its redirects are internal on both sides: it can keep the previous help center's URLs working once its address has moved here, and a redirect to another site stays its owner's to add in the dashboard.","tags":["Redirects"],"security":[{"apiKey":["content.write"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RedirectInput"}}}},"responses":{"200":{"description":"`from` already had a redirect, now pointed at `to`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RedirectSaved"}}}},"201":{"description":"Created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RedirectSaved"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanUpgradeRequired"},"403":{"description":"`insufficient_scope`: the key lacks the scope (a read key writes). `agent_not_allowed`: the help center is unclaimed and this is not its AI agent's key. `capability_locked`: an AI agent's key whose help center has not unlocked redirects yet; `requires` lists what is left and `next_step` what its user does next (the claim link while unclaimed). `api_key_required`: an OAuth token; redirects take an API key of the help center.","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/InsufficientScopeError"},{"$ref":"#/components/schemas/AgentNotAllowedError"},{"$ref":"#/components/schemas/CapabilityLockedError"},{"$ref":"#/components/schemas/ApiKeyRequiredError"}]}}}},"404":{"description":"The feature is off here, which every redirects route answers whatever the key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"410":{"$ref":"#/components/responses/AgentKeyExpired"},"422":{"description":"`validation_error`: the body's shape (`errors` names the fields): `from` must be a path of the help center with no scheme, host, query or fragment, and not the home page; `to` a path starting with `/` or an http(s) URL. Otherwise one of the rules every redirect is saved by, nothing saved: `source_not_redirectable`, `target_not_found`, `external_target_forbidden` (a key not granted \"Allow redirects to other sites\", and every AI agent's key), `redirect_loop`, `too_many_hops`.","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/ValidationError"},{"$ref":"#/components/schemas/RedirectRefusedError"}]}}}},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/v1/redirects/bulk":{"post":{"operationId":"bulkCreateRedirects","summary":"Create or re-point up to 200 redirects","description":"`POST /v1/redirects` for up to 200 items in one request, saved in order (a later item for the same `from` wins). Past the batch's own shape, always answers 207 with one outcome per item, in order: `created`, `updated` or `error`, with the code the single create would have answered. A failed item never fails the batch.","tags":["Redirects"],"security":[{"apiKey":["content.write"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["redirects"],"properties":{"redirects":{"type":"array","minItems":1,"maxItems":200,"items":{"$ref":"#/components/schemas/RedirectInput"},"description":"The redirects, as a list."}}}}}},"responses":{"207":{"description":"Per-item outcomes.","content":{"application/json":{"schema":{"type":"object","required":["status","results","meta"],"properties":{"status":{"const":"success","description":"Always `success`, even when every item failed; read `meta.failed` and each result."},"results":{"type":"array","items":{"$ref":"#/components/schemas/RedirectBulkItemResult"}},"meta":{"type":"object","required":["total","created","updated","failed"],"properties":{"total":{"type":"integer","description":"Number of entries in `results` (one per item sent)."},"created":{"type":"integer"},"updated":{"type":"integer"},"failed":{"type":"integer"}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanUpgradeRequired"},"403":{"description":"`insufficient_scope`: the key lacks the scope (a read key writes). `agent_not_allowed`: the help center is unclaimed and this is not its AI agent's key. `capability_locked`: an AI agent's key whose help center has not unlocked redirects yet; `requires` lists what is left and `next_step` what its user does next (the claim link while unclaimed). `api_key_required`: an OAuth token; redirects take an API key of the help center.","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/InsufficientScopeError"},{"$ref":"#/components/schemas/AgentNotAllowedError"},{"$ref":"#/components/schemas/CapabilityLockedError"},{"$ref":"#/components/schemas/ApiKeyRequiredError"}]}}}},"404":{"description":"The feature is off here, which every redirects route answers whatever the key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"410":{"$ref":"#/components/responses/AgentKeyExpired"},"422":{"$ref":"#/components/responses/ValidationFailed"},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/v1/redirects/{redirectId}":{"delete":{"operationId":"deleteRedirect","summary":"Delete a redirect","description":"The path goes back to the help center's not-found page.","tags":["Redirects"],"security":[{"apiKey":["content.write"]}],"parameters":[{"name":"redirectId","in":"path","required":true,"schema":{"type":"string"},"description":"The redirect's `id`."}],"responses":{"200":{"description":"Deleted.","content":{"application/json":{"schema":{"type":"object","required":["status","redirect"],"properties":{"status":{"const":"success"},"redirect":{"type":"object","required":["id","deleted"],"properties":{"id":{"type":"integer"},"deleted":{"const":true}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PlanUpgradeRequired"},"403":{"description":"`insufficient_scope`: the key lacks the scope (a read key writes). `agent_not_allowed`: the help center is unclaimed and this is not its AI agent's key. `capability_locked`: an AI agent's key whose help center has not unlocked redirects yet; `requires` lists what is left and `next_step` what its user does next (the claim link while unclaimed). `api_key_required`: an OAuth token; redirects take an API key of the help center.","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/InsufficientScopeError"},{"$ref":"#/components/schemas/AgentNotAllowedError"},{"$ref":"#/components/schemas/CapabilityLockedError"},{"$ref":"#/components/schemas/ApiKeyRequiredError"}]}}}},"404":{"description":"No redirect with this id on this help center, or the feature is off here, which every redirects route answers whatever the key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"410":{"$ref":"#/components/responses/AgentKeyExpired"},"429":{"$ref":"#/components/responses/TooManyRequests"}}}}},"components":{"securitySchemes":{"apiKey":{"type":"apiKey","in":"header","name":"apikey","description":"An API key for one help center, issued in the dashboard. Read keys hold content.read and analytics.read; write keys add content.write, webhooks.manage and embed.manage; notes.* and design.* only when the key was issued with them; domain.manage only on a write key granted \"Allow managing the custom domain\" in Settings."},"oauth2":{"type":"oauth2","description":"An OAuth 2.0 access token, sent as `Authorization: Bearer <token>`. Authorization code flow with PKCE (S256 only). A token is never worth more than its person's role on the help center it acts on.","flows":{"authorizationCode":{"authorizationUrl":"https://helpcenter.io/oauth/authorize","tokenUrl":"https://helpcenter.io/oauth/token","refreshUrl":"https://helpcenter.io/oauth/token","scopes":{"openid":"OpenID Connect: confirm the person's identity.","profile":"Their basic profile information (name, picture).","email":"Their email address.","offline_access":"Accepted for clients that ask for it. Every client gets a refresh token whether or not it does.","content.read":"Read articles, categories, images, comments, staged changes, change sets and interface strings, and list the help centers the token can act on.","content.write":"Create, change and delete articles, categories, images, staged changes, change sets, comments and interface strings.","analytics.read":"Read analytics reports.","webhooks.manage":"Register, list and delete webhooks.","notes.read":"Read the team's private editorial notes on articles. Readers never see these.","notes.write":"Reply to and resolve the team's private editorial notes.","design.read":"See the help center's design: its theme, page layouts, components, custom CSS and scripts.","design.write":"Change the help center's design (theme, layouts, components, custom CSS and scripts) and publish it.","domain.manage":"Set, check and remove the help center's custom domain. Held to an Owner or Admin role on the help center.","embed.manage":"Change which websites may embed the help center and run its widget: its embedding origins, which set the `frame-ancestors` header on every help-center page. Held to an Owner or Admin role on the help center."}}}},"migrationStatusToken":{"type":"http","scheme":"bearer","description":"A migration request's `status_token`, from the `201` that filed it, sent as `Authorization: Bearer <status_token>`. It reads that one request's status and nothing else: it is not an API key or an OAuth token, and no other credential reads the status."}},"parameters":{"XHCioSite":{"name":"X-HCio-Site","in":"header","required":false,"description":"OAuth tokens only: the id of the help center to act on, from `GET /v1/sites`. Required when the token's account has more than one help center. API keys ignore it and act on their own help center.","schema":{"type":"integer"}},"SiteQuery":{"name":"site","in":"query","required":false,"description":"The same as the `X-HCio-Site` header, for clients that cannot set headers. The header wins when both are sent.","schema":{"type":"integer"}}},"responses":{"Unauthorized":{"description":"No credential, or one that is unknown, revoked or expired.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UnauthorizedError"}}}},"AgentKeyExpired":{"description":"An AI agent's key whose unclaimed help center expired before anyone claimed it: the help center is deleted, or about to be, along with the key. Answered until 30 days after the deletion, and 401 from then on.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentKeyExpiredError"}}}},"SiteRequired":{"description":"`site_required`: an OAuth token whose account has more than one help center named none. `sites` lists the ids it may name.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SiteRequiredError"}}}},"Forbidden":{"description":"`insufficient_scope`: the credential lacks the scope. `role_forbidden`: an OAuth token holds the scope but its person's role on the help center does not allow it. `site_forbidden`: the named help center is not part of the token's account (the same answer whether or not it exists). `agent_not_allowed`: the help center is unclaimed, and this route, or this credential, is not one it answers; or an AI agent's key, kept at the claim to finish setting up the help center, on a route outside its content, design, redirects and custom domain.","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/InsufficientScopeError"},{"$ref":"#/components/schemas/RoleForbiddenError"},{"$ref":"#/components/schemas/SiteForbiddenError"},{"$ref":"#/components/schemas/AgentNotAllowedError"},{"$ref":"#/components/schemas/Error"}]}}}},"PlanUpgradeRequired":{"description":"`plan_upgrade_required`: only for requests from the HelpCenter.io MCP connector (an `X-HCio-Client: mcp` header, or a token issued for the MCP server) to a help center whose plan does not include MCP access.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlanUpgradeRequiredError"}}}},"NotFound":{"description":"The resource does not exist on this help center, or the credential may not see it.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"ValidationFailed":{"description":"The request failed validation. `errors` maps each failing field to its messages.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}}},"TooManyRequests":{"description":"A rate limit was reached. Wait `Retry-After` seconds.","headers":{"Retry-After":{"description":"Seconds until the limit resets.","schema":{"type":"integer"}},"X-RateLimit-Limit":{"description":"Requests allowed per window.","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests left in the window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix time at which the limit resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitError"}}}}},"schemas":{"Error":{"type":"object","required":["status","message"],"properties":{"status":{"const":"error"},"code":{"type":"string","description":"Machine-readable reason, where the refusal has one."},"message":{"type":"string"}}},"UnauthorizedError":{"type":"object","required":["status"],"properties":{"status":{"const":"unauthorized"},"docs_url":{"type":"string","format":"uri","description":"Where to learn how to make a first request: creating an API key and sending it. Sent when the request carried no credential, or one that is unknown, revoked or expired."},"create_url":{"type":"string","format":"uri","description":"`POST /v1/agent/help-centers`, where an AI agent without an account creates a help center for its user. Only where AI agents may create help centers."},"agent_docs_url":{"type":"string","format":"uri","description":"The guide to migrating a knowledge base as an AI agent. Only where AI agents may create help centers."}}},"AgentKeyExpiredError":{"allOf":[{"$ref":"#/components/schemas/Error"}],"required":["code","site_id"],"properties":{"code":{"const":"expired"},"site_id":{"type":"integer","description":"The help center the key was made for."}}},"InsufficientScopeError":{"allOf":[{"$ref":"#/components/schemas/Error"}],"required":["code"],"properties":{"code":{"const":"insufficient_scope"}}},"RoleForbiddenError":{"allOf":[{"$ref":"#/components/schemas/Error"}],"required":["code","role"],"properties":{"code":{"const":"role_forbidden"},"role":{"type":["string","null"],"description":"The person's role on the help center, lowercase; null when the request acts across every help center the token can reach."}}},"SiteForbiddenError":{"allOf":[{"$ref":"#/components/schemas/Error"}],"required":["code"],"properties":{"code":{"const":"site_forbidden"}}},"SiteRequiredError":{"allOf":[{"$ref":"#/components/schemas/Error"}],"required":["code","sites"],"properties":{"code":{"const":"site_required"},"sites":{"type":"array","items":{"type":"integer"}}}},"CapabilityForbiddenError":{"allOf":[{"$ref":"#/components/schemas/Error"}],"required":["code","capability"],"properties":{"code":{"const":"capability_forbidden"},"capability":{"type":"string","description":"The permission the person's role lacks."}}},"PlanUpgradeRequiredError":{"allOf":[{"$ref":"#/components/schemas/Error"}],"required":["code","feature","upgrade_url","plans"],"properties":{"code":{"const":"plan_upgrade_required"},"feature":{"const":"mcp"},"upgrade_url":{"type":"string","format":"uri"},"plans":{"type":"array","description":"The plans to upgrade to for MCP access.","items":{"type":"string"}}}},"ValidationError":{"type":"object","required":["status","message","errors"],"properties":{"status":{"const":"validation_error"},"message":{"type":"string","description":"`The request could not be accepted.` on most routes. The image upload routes say `The upload could not be accepted.`, or `The image at source_url could not be imported.` when a download fails."},"errors":{"type":"object","additionalProperties":{"type":"array","items":{"type":"string"}}}}},"RateLimitError":{"type":"object","required":["message"],"properties":{"message":{"type":"string"}}},"AnalyticsRateLimitedError":{"type":"object","description":"The analytics endpoints' own rate-limit refusal. The message states the per-minute ceiling that applies.","required":["status","code","message"],"properties":{"status":{"const":"error"},"code":{"const":"analytics_rate_limited"},"message":{"type":"string"}}},"AnalyticsUnavailableError":{"type":"object","required":["status","code","message"],"properties":{"status":{"const":"error"},"code":{"const":"analytics_unavailable"},"message":{"type":"string"}}},"AnalyticsRange":{"type":"object","description":"The window the report measured. Echoed because both bounds are optional; both ends are inclusive.","required":["from","to","days"],"properties":{"from":{"type":"string","format":"date","description":"First day counted."},"to":{"type":"string","format":"date","description":"Last day counted."},"days":{"type":"integer","description":"Number of days in the window."}}},"AnalyticsTrend":{"type":"object","required":["count","change_pct"],"properties":{"count":{"type":"integer","description":"The figure for the window."},"change_pct":{"type":"number","description":"Percentage change against the equally long window immediately before, rounded to one decimal. 0 when unchanged or when the earlier window's figure was 0."}}},"AnalyticsSummaryResponse":{"type":"object","required":["status","range","summary"],"properties":{"status":{"const":"success"},"range":{"$ref":"#/components/schemas/AnalyticsRange"},"summary":{"type":"object","required":["traffic","ratings","activity","totals"],"properties":{"traffic":{"type":"object","description":"Human traffic across all surfaces.","required":["article_views","visitors","searches","article_views_per_visitor"],"properties":{"article_views":{"$ref":"#/components/schemas/AnalyticsTrend"},"visitors":{"$ref":"#/components/schemas/AnalyticsTrend"},"searches":{"allOf":[{"$ref":"#/components/schemas/AnalyticsTrend"}],"description":"The authoritative search volume for the window."},"article_views_per_visitor":{"allOf":[{"$ref":"#/components/schemas/AnalyticsTrend"}],"description":"Article views divided by visitors, rounded up to a whole number (0 with no visitors)."}}},"ratings":{"type":"object","required":["total"],"properties":{"total":{"type":"integer","description":"Article ratings received in the window."}}},"activity":{"type":"object","description":"Authoring activity in the window.","required":["articles_created","articles_updated","articles_published","articles_unpublished","active_team_members"],"properties":{"articles_created":{"type":"integer"},"articles_updated":{"type":"integer"},"articles_published":{"type":"integer"},"articles_unpublished":{"type":"integer"},"active_team_members":{"type":"integer"}}},"totals":{"type":"object","required":["published_articles"],"properties":{"published_articles":{"type":"integer","description":"Articles published now (not limited to the window). With `lang` other than the default language, only those with a completed translation in it."}}}}}}},"AnalyticsContentArticle":{"type":"object","description":"One article's performance in the window.","required":["article_id","title","slug","status","views_in_range","total_views","ratings","updated_at","_links"],"properties":{"article_id":{"type":"integer"},"title":{"type":["string","null"],"description":"Null when withheld from an OAuth Viewer or Translator."},"slug":{"type":["string","null"],"description":"Null when withheld from an OAuth Viewer or Translator."},"status":{"type":"string","description":"The article's status (drafts are never reported), or `private` when withheld from an OAuth Viewer or Translator."},"views_in_range":{"type":"integer","description":"Views inside the window."},"total_views":{"type":"integer","description":"All-time view count."},"ratings":{"type":"object","description":"Percentage shares of the ratings the article has received (each rounded up, so they can sum to more than 100), not counts.","required":["love_pct","thumbs_up_pct","thumbs_down_pct"],"properties":{"love_pct":{"type":"integer","minimum":0,"maximum":100},"thumbs_up_pct":{"type":"integer","minimum":0,"maximum":100},"thumbs_down_pct":{"type":"integer","minimum":0,"maximum":100}}},"updated_at":{"type":["string","null"],"format":"date-time","description":"When the article was last updated (ISO 8601); null when withheld."},"_links":{"type":"object","required":["view"],"properties":{"view":{"type":["string","null"],"format":"uri","description":"The article on the help center; null when withheld."}}}}},"AnalyticsContentResponse":{"type":"object","required":["status","range","excludes_never_viewed","articles","meta"],"properties":{"status":{"const":"success"},"range":{"$ref":"#/components/schemas/AnalyticsRange"},"excludes_never_viewed":{"const":true,"description":"Articles with no views in the window have no row. To find them, list articles and subtract."},"articles":{"type":"array","items":{"$ref":"#/components/schemas/AnalyticsContentArticle"}},"meta":{"type":"object","required":["page","per_page","total_pages","items_count"],"properties":{"page":{"type":"integer"},"per_page":{"type":"integer"},"total_pages":{"type":"integer","minimum":1,"description":"At least 1, even when there are no rows."},"items_count":{"type":"integer","description":"Rows across all pages."}}}}},"AnalyticsArticleReference":{"type":"object","description":"The article associated with a search query.","required":["article_id","status"],"properties":{"article_id":{"type":"integer"},"title":{"type":"string","description":"Title in the help center's default language. Absent when `status` is `deleted` or `private`."},"status":{"type":"string","description":"`deleted` (the article no longer exists), `trashed` (it is in the trash), the article's own status such as `published` or `draft`, or `private` when withheld from an OAuth Viewer or Translator."}}},"AnalyticsSearchQuery":{"type":"object","description":"One search phrase aggregated over the window. Carries no per-visitor data.","required":["query","searches","unique_visitors","zero_result_searches","no_click_searches","failed_searches","failure_rate","reason","last_searched_at","top_hit","article_clicked"],"properties":{"query":{"type":"string"},"searches":{"type":"integer"},"unique_visitors":{"type":"integer"},"zero_result_searches":{"type":"integer","description":"Searches that matched nothing."},"no_click_searches":{"type":"integer","description":"Searches that returned results nobody clicked."},"failed_searches":{"type":"integer"},"failure_rate":{"type":"number","description":"`failed_searches / searches`, rounded to two decimals; 0 when `searches` is 0."},"reason":{"type":["string","null"],"enum":["0 results","no click","mixed",null],"description":"Why a content gap failed; `mixed` when both kinds of failure occurred. Always null on `frequent_queries` rows."},"last_searched_at":{"type":["string","null"],"description":"Most recent search for this phrase, as the analytics warehouse reports it (not normalised to ISO 8601); null when unknown."},"top_hit":{"anyOf":[{"$ref":"#/components/schemas/AnalyticsArticleReference"},{"type":"null"}],"description":"The article most often returned first."},"article_clicked":{"anyOf":[{"$ref":"#/components/schemas/AnalyticsArticleReference"},{"type":"null"}],"description":"The article most often clicked."}}},"AnalyticsSearchTerm":{"type":"object","description":"One word extracted from search queries, aggregated over the window.","required":["term","searches","unique_visitors","successful_searches","failed_searches","failure_rate"],"properties":{"term":{"type":"string"},"searches":{"type":"integer"},"unique_visitors":{"type":"integer"},"successful_searches":{"type":"integer"},"failed_searches":{"type":"integer"},"failure_rate":{"type":"number","description":"`failed_searches / searches`, rounded to two decimals; 0 when `searches` is 0."}}},"AnalyticsSearchesResponse":{"type":"object","required":["status","range","content_gaps","frequent_queries","common_terms","meta"],"properties":{"status":{"const":"success"},"range":{"$ref":"#/components/schemas/AnalyticsRange"},"content_gaps":{"type":"array","description":"Queries with failed searches that matched nothing or got no click, ranked by failed searches, then failure rate, then unique visitors.","items":{"$ref":"#/components/schemas/AnalyticsSearchQuery"}},"frequent_queries":{"type":"array","description":"Queries ranked by volume.","items":{"$ref":"#/components/schemas/AnalyticsSearchQuery"}},"common_terms":{"type":"array","description":"Single words ranked by volume.","items":{"$ref":"#/components/schemas/AnalyticsSearchTerm"}},"meta":{"type":"object","required":["limit","lists_are_top_n"],"properties":{"limit":{"type":"integer","description":"The row cap applied to each list."},"lists_are_top_n":{"const":true,"description":"Each list is the top N, not a page of a longer list."}}}}},"TranslationsLocaleCode":{"type":"string","description":"A supported language code.","enum":["ar","hy","eu","bg","uk","ca","zh-Hans","zh-Hant","cs","da","nl","en","fi","fr","gl","de","de-AT","de-CH","el","hi","he","hu","id","ga","it","ja","ko","lv","no","fa","pl","pt","ro","ru","es","sv","th","tr","vi","ms","fil","mt","lt","mul"]},"TranslationsLocaleMapInput":{"type":"object","description":"Locale code to text. A string sets that locale's override; `null` or `\"\"` removes it; locales not sent are left unchanged. Any supported language is accepted, enabled on the help center or not.","minProperties":1,"propertyNames":{"$ref":"#/components/schemas/TranslationsLocaleCode"},"additionalProperties":{"type":["string","null"],"maxLength":2000}},"TranslationsInterfaceString":{"type":"object","required":["key","label","description","default","group","catalogued","text"],"properties":{"key":{"type":"string"},"label":{"type":["string","null"],"description":"Null when not catalogued."},"description":{"type":["string","null"],"description":"Null when not catalogued."},"default":{"type":["string","null"],"description":"The shipped default text, with a `{customizerProp|fallback}` placeholder resolved to its fallback. Null when not catalogued."},"group":{"type":["string","null"]},"catalogued":{"type":"boolean","description":"False for a stored key the catalogue no longer lists: the dashboard has no field for it and nothing renders it, but it can be read and cleared here."},"text":{"type":"object","description":"This help center's overrides, locale code to text (narrowed to `lang` when given on the list endpoint). `{}` when there are none.","additionalProperties":{"type":"string"}}}},"TranslationsWriteWarnings":{"type":"array","description":"Writes that were stored but will not render: an `en` value for a `widget.*` key (the widget uses its built-in English copy), or a locale that is not enabled on the help center. Empty when there is nothing to report.","items":{"type":"string"}},"WebhookSubscription":{"type":"object","required":["id","url","site_id","active","last_delivered_at","created_at"],"properties":{"id":{"type":"integer"},"url":{"type":"string"},"site_id":{"type":"integer","description":"The one help center this subscription covers."},"active":{"type":"boolean","description":"Inactive subscriptions are listed and deletable but receive no deliveries. New subscriptions are active."},"last_delivered_at":{"type":["string","null"],"format":"date-time","description":"Last successful delivery (ISO 8601), or null."},"created_at":{"type":["string","null"],"format":"date-time"}}},"WebhookSubscriptionWithSecret":{"allOf":[{"$ref":"#/components/schemas/WebhookSubscription"},{"type":"object","required":["secret"],"properties":{"secret":{"type":"string","description":"The HMAC signing secret: the one supplied, or a generated 48-character one. Returned only at creation."}}}]},"WebhooksSiteHasNoOwnerError":{"type":"object","required":["status","code","message"],"properties":{"status":{"const":"error"},"code":{"const":"site_has_no_owner"},"message":{"type":"string"}}},"Article":{"type":"object","description":"An article as every /v1 endpoint that returns one serializes it (App\\Http\\Resources\\PublicResources\\Article): the list, the single read, and the create / update / bulk results all emit this exact shape. No key is conditional and no request parameter changes it. Language maps (`title`, `slug`, `content`, `published_translations`) are encoded by PHP, which writes an empty map as an empty JSON array `[]`, so a map with no languages arrives as `[]`, not `{}`; `metadata`'s maps are always objects. After a create (201, or a new article inside a bulk batch) the response is built from the in-memory row: `type` is null unless `type` was sent, and `views` is null.","required":["id","external_id","title","slug","author","content","category_id","categories","type","published","has_staged_changes","staged_locales","staged_updated_at","visibility","share_token","views","audio_file","metadata","ratings","published_translations","shared_with","created_at","updated_at","_links"],"properties":{"id":{"type":"integer"},"external_id":{"type":["string","null"],"description":"The client-owned key the article was created or adopted under (exact, case-sensitive). Null when it has none."},"title":{"type":["object","array"],"maxItems":0,"additionalProperties":{"type":"string"},"description":"Title per language code."},"slug":{"type":["object","array"],"maxItems":0,"additionalProperties":{"type":"string"},"description":"URL slug per language code."},"author":{"type":"object","required":["id","name","avatar"],"properties":{"id":{"type":"integer"},"name":{"type":["string","null"],"description":"The author's first and last name; null once the author's account is deleted."},"avatar":{"type":["string","null"],"description":"Avatar URL, or null."}}},"content":{"type":["object","array"],"maxItems":0,"additionalProperties":{"type":["string","null"]},"description":"Body HTML per language code, with team-only material removed (internal notes, editor comment highlights, pending AI-draft blocks) and custom embeds sandboxed."},"category_id":{"type":"integer","description":"The article's category: a category id, `0` when uncategorised, or `-1` when it is filed under several categories (listed in `categories`)."},"categories":{"type":"array","items":{"type":"integer"},"description":"Ids from the article's multi-category list. On the OAuth lane a Viewer or Translator is not shown categories kept from them."},"type":{"type":["string","null"],"description":"`article` or `faq`. Null in the response to a create that did not send `type` (the stored default is `article`)."},"published":{"type":"boolean","description":"Whether the live version is published."},"has_staged_changes":{"type":"boolean","description":"Whether unpublished staged edits exist beside the live version. Does not affect `published`."},"staged_locales":{"type":"array","items":{"type":"string"},"description":"Languages that have a staged version."},"staged_updated_at":{"type":["string","null"],"format":"date-time","description":"When staged edits last changed (ISO 8601 with offset), or null."},"visibility":{"type":"string","enum":["public","private","link_only"],"description":"`public` when the stored value is empty."},"share_token":{"type":["string","null"],"description":"Token for the share link of a `link_only` article (32 alphanumeric characters when minted by this API); null when none was minted."},"views":{"type":["integer","null"],"description":"Stored view count. Null in the response to a create."},"audio_file":{"type":["object","null"],"additionalProperties":{"type":["string","null"]},"description":"Audio file per language code, or null when there is none."},"metadata":{"type":"object","required":["title","description"],"additionalProperties":false,"description":"SEO metadata. Only languages with a non-empty value appear; other keys stored by the editor (canonical, robots, og) are never returned.","properties":{"title":{"type":"object","additionalProperties":{"type":"string"},"description":"Meta title per language code."},"description":{"type":"object","additionalProperties":{"type":"string"},"description":"Meta description per language code."}}},"ratings":{"type":"object","required":["thumbs_up","thumbs_down","love"],"description":"Share of all the article's ratings, as a whole-number percentage. Each value is a numeric string (`number_format`) when the article has any ratings, and the integer `0` when it has none. Ratings with score 0 count toward the total but toward none of the three.","properties":{"thumbs_up":{"type":["string","integer"]},"thumbs_down":{"type":["string","integer"]},"love":{"type":["string","integer"]}}},"published_translations":{"type":["object","array"],"maxItems":0,"additionalProperties":{"type":["string","boolean","null"]},"description":"Languages whose translation is released to readers (the default language is never listed). The value is the release time as `Y-m-d H:i:s` or `true`, depending on which path released it."},"shared_with":{"type":"array","items":{"type":"integer"},"description":"Ids of users the article is shared with."},"created_at":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}$","description":"`Y-m-d H:i:s`, application time (UTC), no offset."},"updated_at":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}$","description":"`Y-m-d H:i:s`, application time (UTC), no offset."},"_links":{"type":"object","required":["view"],"properties":{"view":{"type":"object","required":["method","url"],"properties":{"method":{"const":"GET"},"url":{"type":"string","format":"uri","description":"The article's page on the help center (`/content/{slug}`, on the custom domain when one is enabled). Built for drafts and private articles too."}}}}}}},"ArticleWriteFields":{"type":"object","description":"Article fields accepted by `PATCH /v1/articles/{articleId}`, and (with `title` required) by `POST /v1/articles` and each item of `POST /v1/articles/bulk`. Keys not listed are ignored. Language-map keys (`title`, `slug`, `content`) must be the help center's default language or one it has enabled; any other key is refused with 400 `Unsupported language key \"xx\" provided.` (`invalid_argument` in a bulk item). On update, a field or language that is not sent keeps its value.","properties":{"title":{"type":"object","additionalProperties":{"type":"string","minLength":1},"description":"Title per language code. Every value must be non-blank text: a blank or null title is refused, never read as removing that language."},"slug":{"type":"object","additionalProperties":{"type":["string","null"]},"description":"Slug per language code. A sent slug is normalised, made unique in its language (a random suffix is added on collision) and marked hand-written. An empty string or null regenerates it from the title. A language gaining a title without a slug gets one generated; a title change alone never moves an existing slug. When a stored slug changes, the old URL redirects to the new one. A resulting slug longer than 191 characters is refused with 400 before anything is written."},"content":{"type":"object","additionalProperties":{"type":"string"},"description":"Body per language code: HTML, or Markdown when `content_format` is `markdown`. Script vectors are stripped on save. On create, an article without content gets an empty body in the default language. On update, a language sent replaces that language's body and discards its editor document."},"category_id":{"type":"integer","description":"One of this help center's category ids, `0` (uncategorised) or `-1`. Defaults to `0` on create. Requires the `categories.manage` capability."},"categories":{"type":"array","items":{"type":"integer"},"description":"Category ids of this help center (integers; compared strictly). A non-empty list replaces the article's category list and sets `category_id` to `-1`; an empty list is ignored. Requires the `categories.manage` capability."},"type":{"type":"string","enum":["article","faq"]},"published":{"type":"boolean","description":"Create: `true` publishes (needs `articles.publish`); `false` or omitted makes a draft. Update: `true` publishes (needs `articles.publish` if it is a draft), `false` unpublishes (needs `articles.unpublish` if it is published). Laravel's boolean rule also accepts 0, 1, \"0\" and \"1\"."},"visibility":{"type":"string","enum":["public","private","link_only"],"description":"Requires the `content.share` capability. `link_only` mints a share token (on update, only when the article has none, so links already shared keep working)."},"external_id":{"type":["string","null"],"maxLength":191,"description":"A stable key the client owns, unique per help center and matched exactly (case- and accent-sensitive) after trimming surrounding whitespace; blank or null is ignored. On create, an article already under this key is updated instead. On update, the article is re-keyed to it; 409 if another existing article holds it."},"content_format":{"type":["string","null"],"enum":["html","markdown",null],"default":"html","description":"`markdown` converts every `content` value to HTML before it is stored."},"rehost_images":{"type":["boolean","null"],"description":"Copy external `http(s)` and inline `data:` images in `content` `src` attributes onto the help center's own storage and rewrite the URLs (up to 25 images per article; failures keep the original `src` and are reported in `images`). Null or omitted uses the default: on for writes made through the MCP connector, off otherwise. While the help center is unclaimed, an image that does not fit in the 500 MB of images and files it may store refuses the whole write with `422 limit_reached` (`storage_bytes`)."},"metadata":{"type":["object","null"],"additionalProperties":false,"description":"SEO metadata, merged per language: languages not sent keep their value; a null or empty value clears that language. Keys must be enabled languages of the help center (a validation error otherwise).","properties":{"title":{"type":["object","null"],"additionalProperties":{"type":["string","null"],"maxLength":60},"description":"Meta title per language code, at most 60 characters."},"description":{"type":["object","null"],"additionalProperties":{"type":["string","null"],"maxLength":160},"description":"Meta description per language code, at most 160 characters."}}}}},"ArticleCreateRequest":{"description":"Body of `POST /v1/articles` and of each `POST /v1/articles/bulk` item: the article fields with `title` required and non-empty.","allOf":[{"$ref":"#/components/schemas/ArticleWriteFields"},{"type":"object","required":["title"],"properties":{"title":{"type":"object","minProperties":1}}}]},"ChangeReference":{"type":"object","additionalProperties":false,"required":["label","url"],"description":"One link on a change note. Exactly `label` and `url`: any other key is refused.","properties":{"label":{"type":"string","minLength":1,"maxLength":80,"description":"What the link is, as plain text on one line, at most 80 characters after cleaning (control and invisible characters are removed and every run of white space becomes one space). Must not be empty."},"url":{"type":"string","format":"uri","maxLength":2048,"pattern":"^[hH][tT][tT][pP][sS]://","description":"An `https://` address of a named host, at most 2,048 characters. Refused: any other scheme (`http:`, `javascript:`, `data:`, `ftp:`, a scheme-relative `//host`), an IP address in any notation (including `https://2130706433/` and `https://0x7f.0.0.1/`), `localhost` and `*.localhost`, credentials before the host (`user:pass@`), a port outside 1-65535, and spaces, control or invisible characters, backslashes and any of `< > \" ` in the address. The host must also be a public-style name: at least two labels (a single-label host such as `https://intranet/` is refused), each label letters, digits and hyphens only and not starting or ending with a hyphen (so an underscore or an empty label is refused), at most 63 characters a label and 253 in all, and a last label of two or more letters or an `xn--` name (a numeric last label such as `.123` is refused). An internationalised host is converted to its `xn--` form for these checks and for the stored `host`. The address itself is kept as sent."}}},"ChangeNoteFields":{"type":"object","description":"Optional on the writes that change an article: `POST /v1/articles`, `PATCH /v1/articles/{articleId}`, `PATCH /v1/articles/{articleId}/staged`, `POST /v1/articles/{articleId}/staged/publish` and `POST /v1/change-sets/{changeSetId}/publish`. What you say about the change, kept with its activity record and never public. The other article writes ignore both fields without reading them: a bad value is not refused and nothing is stored. They are the bulk create (`POST /v1/articles/bulk`, for every item), restoring a version (`POST /v1/articles/{articleId}/versions/{versionId}/restore`), deleting an article (`DELETE /v1/articles/{articleId}`) and discarding staged changes (`DELETE /v1/articles/{articleId}/staged`). A write that records no change (nothing differs, nothing is staged) keeps no note. The address of a reference is held to more than \"no IP address, no `localhost`, no credentials\": it must be an `https://` address of a real host name, so a host with a single label (`https://intranet/`), with an underscore, with an empty or hyphen-edged label, or whose last label is a number or has digits in it (`https://example.123/`) is refused, and so is any space or any of `< > \" ` and a backslash in the address. See `ChangeReference.url` for the whole rule.","properties":{"change_note":{"type":["string","null"],"maxLength":2000,"description":"What you are saying about this change, in plain text: why it was made, what it fixes, where it came from. At most 2,000 characters after cleaning. Cleaning removes control characters other than newline and tab and invisible format characters (zero-width and bidirectional marks), folds runs of more than two newlines into two and trims the ends; a note that comes out empty is ignored. It is stored exactly as written and is text, never markup. Never public: it is kept with the change in the help center's activity record, for the team, and is not part of any article, API read, webhook, search result or help center page."},"change_refs":{"type":["array","null"],"maxItems":5,"items":{"$ref":"#/components/schemas/ChangeReference"},"description":"Up to 5 links that back the note up: a ticket, a pull request, a source document. Items are counted as sent; two with the same address are stored once. Each link is shown to the team with its real host beside the label, so a label cannot disguise where a link goes. Never public, like `change_note`. Can be sent without a note."}}},"ArticleImageImportReport":{"type":"object","description":"What image re-hosting did for one article.","required":["rehosted","reused","failed","failed_count"],"properties":{"rehosted":{"type":"integer","description":"Distinct sources copied into new stored images."},"reused":{"type":"integer","description":"Distinct sources whose bytes matched an image already stored for the help center."},"failed":{"type":"array","maxItems":100,"description":"Distinct sources that could not be re-hosted (at most 100); their `src` was left as it was.","items":{"type":"object","required":["src","reason"],"properties":{"src":{"type":"string","description":"The source as sent, cut at 2 KB; inline data is reported as `[inline data image, N bytes]`."},"reason":{"type":"string"}}}},"failed_count":{"type":"integer","description":"Failed image occurrences, counting repeats of the same source."}}},"ArticleWriteResult":{"type":"object","description":"Response of `POST /v1/articles`.","required":["status","action","article"],"properties":{"status":{"const":"success"},"action":{"type":"string","enum":["created","updated"]},"article":{"$ref":"#/components/schemas/Article"},"images":{"$ref":"#/components/schemas/ArticleImageImportReport","description":"Present only when image re-hosting ran: it was enabled for this request and `content` was sent."}}},"ArticleBulkItemResult":{"description":"One item's outcome in `POST /v1/articles/bulk`.","oneOf":[{"type":"object","required":["index","status","article"],"properties":{"index":{"type":["integer","string"],"description":"The item's position in `articles` (its key, if `articles` was sent as an object)."},"status":{"type":"string","enum":["created","updated"]},"article":{"$ref":"#/components/schemas/Article"},"images":{"$ref":"#/components/schemas/ArticleImageImportReport","description":"Present only when image re-hosting ran for this item."}}},{"type":"object","required":["index","status","error"],"properties":{"index":{"type":["integer","string"],"description":"The item's position in `articles` (its key, if `articles` was sent as an object)."},"status":{"const":"error"},"external_id":{"description":"The item's `external_id` exactly as sent (null if it had none). Absent when the item was not an object."},"error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string","enum":["invalid_item","validation_error","invalid_argument","external_id_busy","persist_failed","capability_forbidden","publication_forbidden","limit_reached"],"description":"`invalid_item`: not an object. `validation_error`: failed the create rules (`errors` holds the field messages). `invalid_argument`: an unsupported language key or a slug over 191 characters. `external_id_busy`: another request holds the key's lock. `persist_failed`: the write failed (details only in the server log). `capability_forbidden` / `publication_forbidden`: the refusal `POST /v1/articles` would answer with 403. `limit_reached`: the help center is unclaimed and holds 500 articles, or an image `rehost_images` copies does not fit in the 500 MB of images and files it may store; the item was not written."},"message":{"type":"string"},"errors":{"type":"object","additionalProperties":{"type":"array","items":{"type":"string"}},"description":"Only with `validation_error`: field path to messages."},"capability":{"type":"string","description":"Only with `capability_forbidden` / `publication_forbidden`: the capability missing."},"staged_endpoint":{"type":"string","description":"Only with `publication_forbidden`: `/v1/articles/{id}/staged` for the published article."},"limit":{"type":"string","description":"Only with `limit_reached`: `articles` or `storage_bytes`."},"max":{"type":"integer","description":"Only with `limit_reached`: the limit's size (bytes, for `storage_bytes`)."},"migration_request_endpoint":{"const":"POST /v1/agent/migration-requests","description":"Only with `limit_reached`: where to file a migration request."}}}}}]},"ArticleFeedbackResult":{"type":"object","required":["status","action","feedback"],"properties":{"status":{"const":"success"},"action":{"type":"string","enum":["created","updated"]},"feedback":{"type":"object","required":["article_id","vote","score","lang","recorded_at"],"properties":{"article_id":{"type":"integer"},"vote":{"type":"string","enum":["helpful","not_helpful","love"]},"score":{"type":"integer","enum":[1,-1,2]},"lang":{"type":"string","description":"The language the vote was recorded under, after falling back to the default."},"recorded_at":{"type":"string","format":"date-time","description":"ISO 8601 with offset."}}}}},"ArticlesListValidationError":{"type":"object","description":"`GET /v1/articles` parameter validation failure (400). Unlike the shared envelope it has no `message`.","required":["status","errors"],"properties":{"status":{"const":"validation_error"},"errors":{"type":"object","additionalProperties":{"type":"array","items":{"type":"string"}}}}},"ArticlesFieldErrors":{"type":"object","description":"`POST /v1/articles` field validation failure (400): Laravel's message bag serialized bare, field path to messages, with no `status` or `message`.","minProperties":1,"additionalProperties":{"type":"array","items":{"type":"string"}}},"ArticlesPublicationForbiddenError":{"description":"The article is published, and this write would change the live article, but the caller cannot publish.","allOf":[{"$ref":"#/components/schemas/Error"},{"type":"object","required":["code","capability","staged_endpoint"],"properties":{"code":{"const":"publication_forbidden"},"capability":{"const":"articles.publish"},"staged_endpoint":{"type":"string","description":"`/v1/articles/{id}/staged` for this article: where the change can be staged for an editor to publish."}}}]},"ArticlesExternalIdConflictError":{"description":"The `external_id` already belongs to another existing article.","allOf":[{"$ref":"#/components/schemas/Error"},{"type":"object","required":["conflicting_article_id"],"properties":{"conflicting_article_id":{"type":"integer","description":"The article that holds the key."}}}]},"Comment":{"type":"object","description":"A reader comment as the public API reports it. Internal editorial notes are never returned as comments, and the commenter's IP address is never included.","required":["id","article_id","parent_id","lang","status","deleted","comment","author","rating","created_at","updated_at","_links"],"properties":{"id":{"type":"integer"},"article_id":{"type":"integer"},"parent_id":{"type":["integer","null"],"description":"The comment this one replies to; null for a top-level comment."},"lang":{"type":"string","description":"Language code the comment belongs to."},"status":{"type":"string","enum":["pending","published","unpublished"],"description":"Moderation state. A soft-deleted comment keeps the status it had; see `deleted`."},"deleted":{"type":"boolean","description":"Whether the comment is soft-deleted (restorable from the dashboard)."},"comment":{"type":"string","description":"The body. Flattened to plain text whichever way it was stored (visitor comments HTML-escaped, staff-written ones as HTML): tags stripped, entities decoded, tags stripped again, then trimmed — so text that looks like an HTML tag is dropped even when it was typed literally."},"author":{"type":"object","required":["name","type","user_id"],"properties":{"name":{"type":["string","null"],"description":"The linked account's first and last name for a signed-in commenter, otherwise the name the visitor gave (null if none)."},"type":{"type":"string","enum":["user","visitor"]},"user_id":{"type":["integer","null"],"description":"The commenter's user id when `type` is `user`, else null."},"email":{"type":["string","null"],"description":"Only present when the request passed `include_email=1` (the two list endpoints). The account's email for a `user`, else the email the visitor gave (null if none)."}}},"rating":{"type":"integer","description":"The comment's stored `rating` value (0 by default)."},"created_at":{"type":["string","null"],"format":"date-time"},"updated_at":{"type":["string","null"],"format":"date-time"},"replies":{"type":"array","items":{"$ref":"#/components/schemas/Comment"},"description":"Only on `GET /v1/articles/{articleId}/comments`, at every depth: replies to this comment, filtered by that request's `status` and `lang` and ordered as loaded."},"article":{"type":"object","required":["id","title"],"description":"The article the comment is on. Present on `GET /v1/comments` and in the PATCH and POST responses; absent on `GET /v1/articles/{articleId}/comments`, whose top level carries the article instead.","properties":{"id":{"type":"integer"},"title":{"type":["string","null"],"description":"The title in the comment's language, else the article's first non-empty title translation, else null."}}},"_links":{"type":"object","description":"Absolute URLs on the API host.","required":["moderate","delete","reply"],"properties":{"moderate":{"type":"object","required":["method","url"],"properties":{"method":{"const":"PATCH"},"url":{"type":"string","format":"uri"}},"description":"`PATCH /v1/comments/{id}`."},"delete":{"type":"object","required":["method","url"],"properties":{"method":{"const":"DELETE"},"url":{"type":"string","format":"uri"}},"description":"`DELETE /v1/comments/{id}`."},"reply":{"type":"object","required":["method","url"],"properties":{"method":{"const":"POST"},"url":{"type":"string","format":"uri"}},"description":"`POST /v1/articles/{article_id}/comments` (send `parent_id` to reply to this comment)."}}}}},"ArticleNote":{"type":"object","description":"An internal editorial note (or a reply to one) that the team left on an article in the editor. Never visible to readers.","required":["id","article_id","parent_id","lang","note","anchor","author","via","resolved","created_at","updated_at","_links"],"properties":{"id":{"type":"integer"},"article_id":{"type":"integer"},"parent_id":{"type":["integer","null"],"description":"The top-level note this one replies to; null for a top-level note."},"lang":{"type":"string","description":"Language of the article version the note was written on."},"note":{"type":"string","description":"The body. Flattened to plain text whichever way it was stored (visitor comments HTML-escaped, staff-written ones as HTML): tags stripped, entities decoded, tags stripped again, then trimmed — so text that looks like an HTML tag is dropped even when it was typed literally."},"anchor":{"type":"object","description":"The passage of the article the note is attached to.","required":["mark_id","quote","highlighted"],"properties":{"mark_id":{"type":["string","null"],"maxLength":64,"description":"Id of the editor highlight the note is anchored to; null when the row has none (replies have none, and neither does a note `annotate_changes` left on a change with no text to point at)."},"quote":{"type":["string","null"],"description":"The text that was highlighted when the note was written, as the article had it."},"highlighted":{"type":"boolean","description":"Whether the live article or its staged copy still carries the highlight. False once the passage was rewritten or deleted (the note stays), and for a note that never had one."}}},"author":{"type":["object","null"],"description":"The team member who wrote it, or \"AI agent (setup)\" for an AI agent's key; null when that user no longer resolves. Never includes an email address.","required":["id","name","avatar"],"properties":{"id":{"type":"integer"},"name":{"type":"string","description":"First and last name."},"avatar":{"type":["string","null"],"description":"The user's `avatar_url`."}}},"via":{"type":["string","null"],"maxLength":191,"description":"The client the note or reply came through: the API key's name, or the OAuth client as the activity log names it (`Unverified app: …` for a name only the client vouches for). Null for one written in the dashboard."},"resolved":{"type":"boolean","description":"Whether the note was resolved (resolving soft-deletes it)."},"created_at":{"type":["string","null"],"format":"date-time"},"updated_at":{"type":["string","null"],"format":"date-time"},"replies":{"type":"array","items":{"$ref":"#/components/schemas/ArticleNote"},"description":"Present on every top-level note this API returns; replies never carry it (one level only). Open replies only, unless the listing passed `include_resolved=1`."},"_links":{"type":"object","description":"Absolute URLs on the API host, built from this row's own id. Only top-level notes can be replied to or resolved, so on a reply both URLs answer 404.","required":["reply","resolve"],"properties":{"reply":{"type":"object","required":["method","url"],"properties":{"method":{"const":"POST"},"url":{"type":"string","format":"uri"}}},"resolve":{"type":"object","required":["method","url"],"properties":{"method":{"const":"POST"},"url":{"type":"string","format":"uri"}}}}}}},"NotesForbiddenError":{"type":"object","description":"Returned by the `api.notes` gate when the user behind the credential does not hold the `notes.read` permission on the resolved help center.","required":["status","code","message"],"properties":{"status":{"const":"error"},"code":{"const":"notes_forbidden"},"message":{"type":"string"}}},"Image":{"type":"object","description":"An image uploaded through the public API (POST /v1/images, POST /v1/images/bulk or a single-use upload URL). Kept apart from the editor's file library.","properties":{"id":{"type":"integer"},"url":{"type":"string","format":"uri","description":"Absolute URL of the stored image; embed it in article content. On a help center readers need permission for, it is a private address that opens only for the team or signed, as the pages that show it paint it."},"filename":{"type":"string","description":"For a file upload, the client's filename with any path and control characters removed (at most 255 characters; a random name when nothing usable is left). For a source URL import, the URL's last path segment without its extension (at most 120 characters, `imported-image` when empty) plus the extension of the detected type. A deduplicated upload returns the filename of the image it resolved to."},"mime_type":{"type":["string","null"],"description":"The type detected from the bytes: image/jpeg, image/png, image/gif or image/webp."},"size":{"type":"integer","description":"Size in bytes."},"checksum":{"type":["string","null"],"description":"Lowercase hex sha256 of the bytes (64 characters) — what deduplication and `GET /v1/images?checksum=` match on. Null when the server could not hash the upload, in which case it was not deduplicated."},"alt_text":{"type":"string","description":"Not stored with the image: the trimmed `alt_text` (or `alt_texts[i]`) of the upload request that returned it, and an empty string from GET /v1/images and GET /v1/images/{imageId}."},"snippets":{"type":"object","description":"Ready-to-paste references built from `url` and `alt_text`.","properties":{"html":{"type":"string","description":"`<img src=\"…\" alt=\"…\">`, both attributes HTML-escaped."},"markdown":{"type":"string","description":"`![alt](url)`, with `\\`, `[` and `]` in the alt text backslash-escaped."}},"required":["html","markdown"]},"created_at":{"type":"string","description":"Upload time formatted `Y-m-d H:i:s`, with no timezone designator."}},"required":["id","url","filename","mime_type","size","checksum","alt_text","snippets","created_at"]},"ImagesStoreResult":{"type":"object","properties":{"status":{"type":"string","const":"success"},"reused":{"type":"boolean","description":"true (HTTP 200) when the bytes matched an image the site already holds and that image is returned; false (HTTP 201) when a new image was stored."},"image":{"$ref":"#/components/schemas/Image"}},"required":["status","reused","image"]},"ImagesBulkResult":{"type":"object","properties":{"status":{"type":"string","const":"success","description":"Always `success`; read each item's own `status`."},"results":{"type":"array","description":"One entry per item, in index order.","items":{"oneOf":[{"type":"object","properties":{"index":{"type":["integer","string"],"description":"The item's key in `files` / `source_urls` (an integer for list-shaped input)."},"status":{"type":"string","enum":["created","reused"]},"image":{"$ref":"#/components/schemas/Image"}},"required":["index","status","image"]},{"type":"object","description":"A failed item carries `filename` (a file item, the client's filename as sent) or `source_url` (a source URL item, as sent).","properties":{"index":{"type":["integer","string"],"description":"The item's key in `files` / `source_urls` (an integer for list-shaped input)."},"status":{"type":"string","const":"error"},"filename":{"type":"string"},"source_url":{"type":"string"},"error":{"type":"object","properties":{"code":{"type":"string","enum":["import_failed","storage_failed","limit_reached"],"description":"`import_failed`: a source URL could not be downloaded or is not an acceptable image (message `The image could not be imported: <reason>.`, including when the batch's download budget ran out). `storage_failed`: the image could not be stored (message `The image could not be stored. Please try again.`). `limit_reached`: the help center is unclaimed and the image does not fit in what it may store (`limit`, `max` and `migration_request_endpoint` as in `LimitReachedError`); nothing was stored for this item."},"message":{"type":"string"},"limit":{"type":"string","description":"Only with `limit_reached`: `storage_bytes`."},"max":{"type":"integer","description":"Only with `limit_reached`: the bytes an unclaimed help center may store."},"migration_request_endpoint":{"const":"POST /v1/agent/migration-requests","description":"Only with `limit_reached`: where to file a migration request."}},"required":["code","message"]}},"required":["index","status","error"]}]}},"meta":{"type":"object","properties":{"total":{"type":"integer"},"created":{"type":"integer"},"reused":{"type":"integer"},"failed":{"type":"integer"}},"required":["total","created","reused","failed"]}},"required":["status","results","meta"]},"ImagesUploadUrlError":{"type":"object","description":"Refusal of a single-use upload URL by VerifyImageUploadUrl (or the controller's atomic claim, for `upload_url_used`).","properties":{"status":{"type":"string","const":"error"},"code":{"type":"string","enum":["upload_url_invalid","upload_url_expired","upload_url_used","upload_url_revoked"]},"message":{"type":"string"}},"required":["status","code","message"]},"ImagesReferencedError":{"type":"object","properties":{"status":{"type":"string","const":"error"},"message":{"type":"string","description":"`This image is still referenced by article content. Update those articles first, or retry with force=true.`"},"referenced_by":{"type":"array","description":"Articles on this site whose content embeds the image URL. An article appears at most once per kind (it may appear under several kinds); each kind is capped at 20.","items":{"type":"object","properties":{"id":{"type":"integer","description":"Article id."},"title":{"type":"object","additionalProperties":{"type":"string"},"description":"The article's title in every locale it has."},"kind":{"type":"string","enum":["live","trashed","staged","archived"],"description":"Where the reference lives: the article as it stands, an article in the Trash, an unpublished staged edit, or an archived earlier version."}},"required":["id","title","kind"]}}},"required":["status","message","referenced_by"]},"Category":{"type":"object","description":"A help center category (PublicResources\\Category). Emitted the same way by every /v1 endpoint that returns categories.","required":["id","parent","name","description","icon","position","privacy","created_at","updated_at"],"properties":{"id":{"type":"integer"},"parent":{"type":["integer","null"],"description":"The parent category's id; null for a top-level category."},"name":{"description":"Every stored translation as a `{locale: text}` map; locales whose value is null or empty are omitted. An empty map is serialised as `[]` (a JSON array), not `{}`.","anyOf":[{"type":"object","additionalProperties":{"type":"string"}},{"type":"array","maxItems":0}]},"description":{"description":"Same shape as `name`. `[]` when the category has no description in any locale.","anyOf":[{"type":"object","additionalProperties":{"type":"string"}},{"type":"array","maxItems":0}]},"icon":{"type":["string","null"],"description":"The category's image URL."},"position":{"type":"integer","description":"Sibling order, ascending; only comparable within the same parent. A category never given a position holds 100000."},"privacy":{"enum":["public","private","team_private","unlisted"],"description":"Integrations serving end users should treat anything but `public` as not for anonymous readers."},"created_at":{"type":"string","description":"`YYYY-MM-DD HH:MM:SS`, no timezone designator."},"updated_at":{"type":"string","description":"`YYYY-MM-DD HH:MM:SS`, no timezone designator."}}},"Site":{"type":"object","description":"A help center as GET /v1/sites presents it (PublicResources\\Site).","required":["id","uuid","name","subdomain","domain","url","default_language","visibility","published","publicly_accessible","languages","created_at"],"properties":{"id":{"type":"integer","description":"The value for `X-HCio-Site` / `?site=`."},"uuid":{"type":"string","format":"uuid","description":"Stable public identifier; the `helpcenter_id` carried by analytics events, webhook envelopes and GET /v1/export."},"name":{"type":"string","description":"The help center's (company) name."},"subdomain":{"type":"string"},"domain":{"type":"string","description":"The host the help center is served on: its custom domain when one is enabled, otherwise `{subdomain}.` plus the platform's root domain."},"url":{"type":"string","format":"uri","description":"The help center's public base URL: the customer's own subfolder when subfolder hosting is live, otherwise `https://` plus `domain`."},"default_language":{"type":"string","description":"Locale code of the default language."},"visibility":{"enum":["public","private","password"],"description":"Who may read the help center once it is published."},"published":{"type":"boolean","description":"True when `visibility` is `public` and the help center's team has published it. A public help center that is not published yet opens for its team alone. Only its team can publish it, from the dashboard."},"publicly_accessible":{"type":"boolean","description":"True only when the help center is `published` and neither suspended nor restricted to an IP allow-list; an OAuth grant to private content does not make that content eligible for anonymous use."},"languages":{"type":"array","items":{"type":"string"},"description":"Locale codes: the default language first, then the additional languages."},"created_at":{"type":["string","null"],"description":"`YYYY-MM-DD HH:MM:SS`, no timezone designator."}}},"AccountResponse":{"type":"object","description":"Body of GET /v1/account.","required":["status","account","provider","plan","kb"],"properties":{"status":{"const":"success"},"account":{"type":"object","description":"The account head (billing owner) behind the credential. For a credential issued by or to a team member, this is the account that owns the team, not the member.","required":["id","email","name"],"properties":{"id":{"type":"integer"},"email":{"type":"string"},"name":{"type":"string"}}},"provider":{"const":"helpcenter.io"},"plan":{"type":"object","required":["name","title","active","on_trial","ai_included","specs","source","via_bundle","bundle"],"properties":{"name":{"type":["string","null"],"description":"Internal plan name; null when the account has no current plan."},"title":{"type":["string","null"],"description":"The plan's display title, falling back to `name`."},"active":{"type":"boolean","description":"True when the account has a HelpCenter.io subscription or an active HelpCenter.chat bundle grant."},"on_trial":{"type":"boolean"},"ai_included":{"type":"boolean","description":"The plan includes AI, or the bundle grant does."},"specs":{"description":"The plan's specification map (values are strings, booleans or null). `[]` when there is no plan.","anyOf":[{"type":"object"},{"type":"array","maxItems":0}]},"source":{"enum":["subscription","chat_bundle","both","none"],"description":"Where the active entitlement comes from."},"via_bundle":{"type":"boolean","description":"Whether an active HelpCenter.chat bundle grant contributes."},"bundle":{"description":"Present (non-null) only for an active version-2 bundle grant.","anyOf":[{"type":"null"},{"type":"object","required":["site_id","kb_plan","paid_through"],"properties":{"site_id":{"type":["integer","null"]},"kb_plan":{"type":["string","null"]},"paid_through":{"type":"string","format":"date-time"}}}]}}},"kb":{"type":"object","required":["sites_count","site_ids","capabilities"],"properties":{"sites_count":{"type":"integer","minimum":0},"site_ids":{"type":"array","items":{"type":"integer"},"description":"API key: exactly the key's help center id (still listed after that help center is deleted). OAuth: every help center the grant reaches."},"capabilities":{"type":"object","description":"What the credential may actually do, not merely what it was granted. API key: `read` is always true, `write` is true for a write key. OAuth: the scope AND the person's role (see the operation description); both false when the grant reaches no help center.","required":["read","write"],"properties":{"read":{"type":"boolean"},"write":{"type":"boolean"}}}}}}},"CategoriesNotEmptyError":{"type":"object","description":"409 body of DELETE /v1/categories/{categoryId} under the `refuse` policy. Counts are of live (non-trashed) content only.","required":["status","code","message","articles_count","children_count"],"properties":{"status":{"const":"error"},"code":{"const":"category_not_empty"},"message":{"type":"string"},"articles_count":{"type":"integer","minimum":0,"description":"Live articles filed in the category."},"children_count":{"type":"integer","minimum":0,"description":"Live sub-categories."}}},"StagedChangesLocaleCode":{"type":"string","description":"A language HelpCenter.io supports (`App\\Language::$supportedLanguages`).","enum":["ar","hy","eu","bg","uk","ca","zh-Hans","zh-Hant","cs","da","nl","en","fi","fr","gl","de","de-AT","de-CH","el","hi","he","hu","id","ga","it","ja","ko","lv","no","fa","pl","pt","ro","ru","es","sv","th","tr","vi","ms","fil","mt","lt","mul"]},"StagedChangesLink":{"type":"object","properties":{"method":{"type":"string","enum":["GET","POST","DELETE"]},"url":{"type":"string","format":"uri","description":"Absolute URL on the API host."}},"required":["method","url"]},"StagedChangesFields":{"type":"object","description":"This locale's own staged fields. Only the fields ever staged for this locale are present (PATCH is partial). The editor's TipTap document (`content_json`) is never included.","properties":{"title":{"type":"string","description":"An empty string means the title is staged as cleared."},"content":{"type":"string","description":"Staged HTML, sanitised for the public API (internal notes, team-comment anchors and pending AI-draft blocks removed)."},"slug":{"type":"string"},"audio_file":{"type":["string","null"],"description":"`null` stages removal of this language's audio file."},"has_custom_slug":{"type":"boolean"}}},"StagedChangesShared":{"type":"object","description":"The article-wide block (not per language). Every healthy staged row of the article carries the same block, and publishing any locale applies it. Only keys that are staged are present.","properties":{"category_id":{"type":"integer","description":"Stored in the live column's vocabulary: `0` = no category, `-1` = filed under several (see `categories`), otherwise a category id."},"categories":{"type":["array","null"],"items":{"type":"integer"},"description":"Category ids (sorted) for a multi-category article."},"visibility":{"type":["string","null"],"enum":["public","private","link_only",null]},"share_token":{"type":["string","null"],"description":"Staged only from the dashboard, together with `visibility: link_only`."},"metadata":{"type":"object","description":"Staged SEO metadata. Only `title` and `description` are exposed (the stored block's `canonical`, `robots` and `og` are withheld).","properties":{"title":{"type":"object","additionalProperties":{"type":"string"}},"description":{"type":"object","additionalProperties":{"type":"string"}}},"required":["title","description"]},"type":{"type":"string","enum":["article","faq"]},"author_id":{"type":["integer","null"]},"shared_with":{"type":"array","items":{"type":"integer"},"description":"Private-access user ids, sorted."}}},"StagedChange":{"type":"object","description":"One locale's staged copy of an article.","properties":{"article_id":{"type":"integer"},"locale":{"type":"string"},"fields":{"anyOf":[{"$ref":"#/components/schemas/StagedChangesFields"},{"type":"array","maxItems":0}],"description":"An empty set of fields is encoded as `[]`, not `{}`."},"shared":{"anyOf":[{"$ref":"#/components/schemas/StagedChangesShared"},{"type":"array","maxItems":0}],"description":"An empty block is encoded as `[]`, not `{}`."},"release_translation":{"type":"boolean","description":"Whether publishing this locale also marks the translation released."},"etag":{"type":["string","null"],"description":"Optimistic-lock token for this locale's fields. Send it back as `etag` on the next PATCH."},"shared_etag":{"type":["string","null"],"description":"Optimistic-lock token for the article-wide block; `null` when none has been minted. Send it back as `shared_etag` when a PATCH changes article-wide fields (or the literal `none` if this was `null`)."},"base_signature":{"type":["string","null"],"description":"Hash of the live values this copy was forked from."},"updated_at":{"type":["string","null"],"format":"date-time"},"_links":{"type":"object","properties":{"publish":{"allOf":[{"$ref":"#/components/schemas/StagedChangesLink"}],"properties":{"method":{"const":"POST"}},"description":"`POST /v1/articles/{articleId}/staged/publish`."},"discard":{"allOf":[{"$ref":"#/components/schemas/StagedChangesLink"}],"properties":{"method":{"const":"DELETE"}},"description":"`DELETE /v1/articles/{articleId}/staged?locale=…` for this locale."},"compare":{"allOf":[{"$ref":"#/components/schemas/StagedChangesLink"}],"properties":{"method":{"const":"GET"}},"description":"`GET /v1/articles/{articleId}?locale=…` (the live article)."}},"required":["publish","discard","compare"]}},"required":["article_id","locale","fields","shared","release_translation","etag","shared_etag","base_signature","updated_at","_links"]},"StagedChangesArchivedVersion":{"type":"object","description":"An archived snapshot of what was live before a publish or restore.","properties":{"id":{"type":"integer"},"locale":{"type":"string"},"title":{"type":["string","null"],"description":"The archived title for this locale."},"published_at":{"type":["string","null"],"format":"date-time","description":"When this content became live (the article's `updated_at` at the time it was archived)."},"_links":{"type":"object","properties":{"restore":{"allOf":[{"$ref":"#/components/schemas/StagedChangesLink"}],"properties":{"method":{"const":"POST"}},"description":"`POST /v1/articles/{articleId}/versions/{versionId}/restore`."}},"required":["restore"]}},"required":["id","locale","title","published_at","_links"]},"StagedChangesPlanUnavailableError":{"type":"object","description":"The help center's plan (or the rollout flag) does not include staged changes.","properties":{"status":{"const":"error"},"code":{"const":"STAGING_UNAVAILABLE"},"message":{"type":"string"}},"required":["status","code","message"]},"StagedChangesNotStageableError":{"type":"object","properties":{"status":{"const":"error"},"code":{"type":"string","enum":["NOT_PUBLISHED","ARTICLE_UNPUBLISHED","CHANGE_SETS_UNAVAILABLE"],"description":"`NOT_PUBLISHED`: the article has never been live (use `PATCH /v1/articles/{articleId}`). `ARTICLE_UNPUBLISHED`: the article was taken down; its staged changes are kept but cannot be added to. `CHANGE_SETS_UNAVAILABLE`: a non-zero `change_set_id` was sent; this endpoint does not accept one."},"message":{"type":"string"}},"required":["status","code","message"]},"StagedChangesStaleEtagError":{"type":"object","description":"The staged copy (scope `locale`) or the article-wide block (scope `shared`) changed since the token the client sent. The current token is returned in `etag` for scope `locale` and in `shared_etag` for scope `shared` (`none` when no block exists); never both.","properties":{"status":{"const":"error"},"code":{"const":"STALE_ETAG"},"message":{"type":"string"},"scope":{"type":"string","enum":["locale","shared"]},"etag":{"type":"string"},"shared_etag":{"type":"string"}},"required":["status","code","message","scope"]},"StagedChangesBaseChangedError":{"type":"object","description":"The live article changed after these edits were staged. Re-read and re-stage, discard, or retry with `force: true`.","properties":{"status":{"const":"error"},"code":{"const":"BASE_CHANGED"},"message":{"type":"string"},"locale":{"type":"string","description":"The staged locale whose base no longer matches."}},"required":["status","code","message","locale"]},"ChangeSetStatus":{"type":"string","enum":["open","publishing","published","partially_published","failed","cancelled"],"description":"`open`, `partially_published` and `failed` can still be edited and published. A scheduled set is still `open` (see `is_scheduled`)."},"ChangeSetSummary":{"type":"object","properties":{"id":{"type":"integer"},"name":{"type":"string"},"description":{"type":["string","null"]},"status":{"$ref":"#/components/schemas/ChangeSetStatus"},"is_scheduled":{"type":"boolean","description":"True when the set is `open`, has a `scheduled_for` and has not yet been claimed by the scheduler."},"scheduled_for":{"type":["string","null"],"format":"date-time"},"published_at":{"type":["string","null"],"format":"date-time"},"release_note":{"type":["string","null"]},"items_count":{"type":"integer"},"processed_items":{"type":"integer"},"total_items":{"type":"integer"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}},"required":["id","name","description","status","is_scheduled","scheduled_for","published_at","release_note","items_count","processed_items","total_items","created_at","updated_at"]},"ChangeSetItem":{"type":"object","properties":{"id":{"type":"integer"},"type":{"type":"string","enum":["staged_version","article_publish","category_visibility"],"description":"`staged_version`: one staged locale of a live article. `article_publish`: publishes a never-published article. `category_visibility`: flips a category's visibility."},"article_id":{"type":["integer","null"]},"category_id":{"type":["integer","null"]},"locale":{"type":["string","null"]},"variant":{"type":"string"},"payload":{"type":["object","null"],"description":"For `category_visibility` items: the visibility captured when the item was added (`from`) and the one it will set (`to`). `null` for article items.","properties":{"from":{"type":["string","null"]},"to":{"type":"string","enum":["public","unlisted","private","team_private"]}}},"applied":{"type":"boolean"},"error":{"type":["string","null"],"description":"Why the item failed in the last publish run, if it did."}},"required":["id","type","article_id","category_id","locale","variant","payload","applied","error"]},"ChangeSetStatusItem":{"type":"object","properties":{"id":{"type":"integer"},"type":{"type":"string","enum":["staged_version","article_publish","category_visibility"]},"article_id":{"type":["integer","null"]},"locale":{"type":["string","null"]},"variant":{"type":"string"},"applied":{"type":"boolean"},"error":{"type":["string","null"]}},"required":["id","type","article_id","locale","variant","applied","error"]},"ChangeSetReadinessBlocker":{"type":"object","description":"Something that stops the set from publishing. `article_gone` (staged or publish item whose article was deleted), `conflict` (the live article changed after the edits were staged; carries `article_id`, `article_title`, `locale`), `category_gone`.","properties":{"item_id":{"type":"integer"},"article_id":{"type":["integer","null"]},"article_title":{"type":["string","null"]},"locale":{"type":"string"},"reason":{"type":"string","enum":["article_gone","conflict","category_gone"]},"message":{"type":"string"}},"required":["item_id","reason","message"]},"ChangeSetReadinessWarning":{"type":"object","description":"Something only the customer can judge; never stops a publish. `already_applied` (a category is already at its target visibility; carries `item_id`), `locale_gap` (the release spans several locales and this article lacks some; carries `article_id`, `article_title`, `missing_locales`), `staged_not_added` (the article has a staged locale that is not in the set; carries `article_id`, `article_title`, `article_version_id`, `locale`).","properties":{"item_id":{"type":"integer"},"article_id":{"type":"integer"},"article_title":{"type":["string","null"]},"article_version_id":{"type":"integer"},"locale":{"type":"string"},"missing_locales":{"type":"array","items":{"type":"string"}},"reason":{"type":"string","enum":["already_applied","locale_gap","staged_not_added"]},"message":{"type":"string"}},"required":["reason","message"]},"ChangeSetReadiness":{"type":"object","properties":{"counts":{"type":"object","properties":{"staged_version":{"type":"integer"},"article_publish":{"type":"integer"},"category_visibility":{"type":"integer"}},"required":["staged_version","article_publish","category_visibility"]},"locales":{"type":"array","items":{"type":"string"},"description":"Distinct locales the set touches, sorted."},"articles":{"type":"integer","description":"Distinct articles the set touches."},"blockers":{"type":"array","items":{"$ref":"#/components/schemas/ChangeSetReadinessBlocker"}},"warnings":{"type":"array","items":{"$ref":"#/components/schemas/ChangeSetReadinessWarning"}},"publishable":{"type":"boolean","description":"True when the set has at least one item and no blockers."}},"required":["counts","locales","articles","blockers","warnings","publishable"]},"ChangeSet":{"description":"A change set with its readiness and items.","allOf":[{"$ref":"#/components/schemas/ChangeSetSummary"},{"type":"object","properties":{"readiness":{"$ref":"#/components/schemas/ChangeSetReadiness"},"items":{"type":"array","items":{"$ref":"#/components/schemas/ChangeSetItem"},"description":"Ordered by position, then id."}},"required":["readiness","items"]}]},"ChangeSetItemRejection":{"type":"object","description":"One id from the request that could not be added. Carries `article_id` or `category_id`, echoed as sent.","properties":{"article_id":{"type":["integer","string"]},"category_id":{"type":["integer","string"]},"reason":{"type":"string","description":"`not_found` when the id is not on this help center (including another help center's ids); otherwise a human-readable message, e.g. a live article with nothing staged."}},"required":["reason"]},"ChangeSetPlanUnavailableError":{"type":"object","properties":{"status":{"const":"error"},"reason":{"const":"CHANGE_SETS_UNAVAILABLE"},"message":{"type":"string"}},"required":["status","reason","message"]},"ChangeSetConflictError":{"type":"object","description":"The set cannot take the request as it stands. `CHANGE_SET_NOT_OPEN`: it is publishing or already published, so its contents cannot change. `CHANGE_SET_NOT_SCHEDULABLE`: it has already been through a publish run (`failed` / `partially_published`) and must be published now rather than scheduled. `CHANGE_SET_FULL`: it already holds 500 items. `TOO_MANY_OPEN_CHANGE_SETS`: the help center already has 25 open sets.","properties":{"status":{"const":"error"},"reason":{"type":"string","enum":["CHANGE_SET_NOT_OPEN","CHANGE_SET_NOT_SCHEDULABLE","CHANGE_SET_FULL","TOO_MANY_OPEN_CHANGE_SETS"]},"message":{"type":"string"}},"required":["status","reason","message"]},"ChangeSetReadinessError":{"type":"object","description":"Publishing was refused because the set is not publishable: it is empty, or `readiness.blockers` is non-empty.","properties":{"status":{"const":"error"},"message":{"type":"string"},"readiness":{"$ref":"#/components/schemas/ChangeSetReadiness"}},"required":["status","message","readiness"]},"TemplateDesignForbiddenError":{"type":"object","required":["status","code","message"],"properties":{"status":{"const":"error"},"code":{"const":"design_forbidden"},"message":{"type":"string"}},"description":"403 from the `api.design` gate: the user behind the credential may no longer edit this help center's design in the dashboard."},"TemplateUnavailableError":{"type":"object","required":["status","code","message"],"properties":{"status":{"const":"error"},"code":{"const":"templating_unavailable"},"message":{"type":"string"}},"description":"409: the v2 Template Editor is switched off on this environment."},"TemplateMigrationRequiredError":{"type":"object","required":["status","code","message","customizer_url"],"properties":{"status":{"const":"error"},"code":{"const":"template_migration_required"},"message":{"type":"string"},"customizer_url":{"type":"string","format":"uri","description":"The dashboard's Template Editor for this help center, where the owner migrates it to v2."}},"description":"409: the help center still uses the classic (v1) template."},"TemplateNothingToPublishError":{"type":"object","required":["status","code","message"],"properties":{"status":{"const":"error"},"code":{"const":"nothing_to_publish"},"message":{"type":"string"}},"description":"409: the draft is identical to the live design."},"TemplateStaleDraftError":{"type":"object","required":["status","code","message","etag"],"properties":{"status":{"const":"error"},"code":{"const":"stale_draft"},"message":{"type":"string"},"etag":{"$ref":"#/components/schemas/TemplateEtag"}},"description":"412: the draft changed since the etag the caller sent. `etag` is the draft's current one."},"TemplateNotFoundError":{"type":"object","required":["status","code","message"],"properties":{"status":{"const":"error"},"code":{"type":"string","enum":["unknown_theme","component_not_found","not_found","version_not_found"]},"message":{"type":"string"}},"description":"404 for an unknown theme, draft component, script or custom CSS item, or version."},"TemplateInvalidError":{"type":"object","required":["status","code","message","errors"],"properties":{"status":{"const":"error"},"code":{"const":"template_invalid"},"message":{"type":"string"},"errors":{"type":"array","items":{"type":"object","required":["message"],"properties":{"operation":{"type":"integer","description":"0-based index of the failing operation."},"op":{"type":"string","description":"The failing operation's `op` (empty string when the entry had no valid `op`)."},"path":{"type":"string","description":"The field or setting path at fault, e.g. `config.columns` or `theme.colors.primary`."},"message":{"type":"string"}}}}},"description":"422: an operation could not be applied, so nothing was saved. `message` starts with \"Nothing was saved. \" and summarises up to ten errors. Usually one entry (the batch stops at the first failing operation); `set`, `update_region` and component `config` checks report every bad key of that operation. When the edited document as a whole is invalid, the entry carries only `message`."},"TemplateCssUrlForbiddenError":{"type":"object","required":["status","code","message","urls"],"properties":{"status":{"const":"error"},"code":{"const":"agent_css_url_forbidden"},"message":{"type":"string"},"urls":{"type":"array","items":{"type":"string"},"description":"Each address the request would have added, decoded as a browser reads it."}},"description":"422, AI agent keys only: the write would add a CSS address outside the help center (a url(), image-set() or src() in custom CSS, @font-face sources included, or a component's backgroundImageUrl), so nothing was changed. An agent key's CSS may load only a relative path, a data: URL or one of this help center's own stored files (POST /v1/images returns their addresses). An address the design already held may stay. Edits, theme applies, restores and publishes are checked; a discard is not, since it only brings back what is live. `message` starts with \"Nothing was changed. \" and names each address and where it is."},"TemplateEtag":{"type":"string","description":"Opaque optimistic-lock token for the draft. The value includes literal double-quote characters around it (draft id, update time in milliseconds and content digests); send it back byte for byte as `etag`."},"TemplateVersion":{"type":"object","required":["id","status","live","message","preset_key","published_at"],"properties":{"id":{"type":"integer"},"status":{"type":"string","enum":["published","archived"]},"live":{"type":"boolean","description":"Whether this is the version visitors see now."},"message":{"type":["string","null"],"description":"The note given at publish time."},"preset_key":{"type":["string","null"],"description":"The theme the version was started from."},"published_at":{"type":["string","null"],"format":"date-time"}}},"TemplateVersionListItem":{"allOf":[{"$ref":"#/components/schemas/TemplateVersion"},{"type":"object","required":["published_by"],"properties":{"published_by":{"type":["string","null"],"description":"Full name of the person who published it, or \"AI agent (setup)\" when an AI agent's key did; null when unknown."}}}]},"TemplateStatus":{"type":"object","required":["engine","site","draft","published","urls"],"properties":{"engine":{"const":"v2"},"site":{"type":"object","required":["id","name","subdomain"],"properties":{"id":{"type":"integer"},"name":{"type":"string"},"subdomain":{"type":"string"}}},"draft":{"type":"object","required":["id","etag","updated_at","preset_key","has_unpublished_changes"],"properties":{"id":{"type":"integer"},"etag":{"$ref":"#/components/schemas/TemplateEtag"},"updated_at":{"type":["string","null"],"format":"date-time"},"preset_key":{"type":["string","null"],"description":"The theme the draft was started from or last had applied."},"has_unpublished_changes":{"type":"boolean","description":"Whether the draft's document or translations differ from the live version (always true when nothing is published)."}}},"published":{"anyOf":[{"$ref":"#/components/schemas/TemplateVersion"},{"type":"null"}],"description":"The live version, or null when none."},"urls":{"type":"object","required":["live_site","customizer","draft_preview"],"properties":{"live_site":{"type":"string","format":"uri"},"customizer":{"type":"string","format":"uri","description":"The dashboard's Template Editor, which opens this same draft with a live preview."},"draft_preview":{"type":"string","format":"uri","description":"Serves the draft to signed-in team members only."}}}}},"TemplateBrand":{"type":"object","required":["name","logoAssetId","faviconAssetId","tagline","logoDarkAssetId","connectedSiteUrl"],"properties":{"name":{"type":"string"},"logoAssetId":{"type":["string","null"],"description":"Logo image URL."},"faviconAssetId":{"type":["string","null"]},"tagline":{"type":["string","null"]},"logoDarkAssetId":{"type":["string","null"]},"connectedSiteUrl":{"type":["string","null"],"description":"The customer's main website. null means the document predates the field; \"\" means it was cleared. The two are distinct."}}},"TemplateTheme":{"type":"object","required":["colors","darkColors","fonts","radius","density"],"properties":{"colors":{"type":"object","required":["primary","accent","fg","bg","muted","surface","border"],"properties":{"primary":{"type":"string"},"accent":{"type":"string"},"fg":{"type":"string"},"bg":{"type":"string"},"muted":{"type":"string"},"surface":{"type":"string"},"border":{"type":"string"}}},"darkColors":{"type":"object","required":["primary","accent","fg","bg","muted","surface","border"],"properties":{"primary":{"type":"string"},"accent":{"type":"string"},"fg":{"type":"string"},"bg":{"type":"string"},"muted":{"type":"string"},"surface":{"type":"string"},"border":{"type":"string"}}},"fonts":{"type":"object","required":["body","heading","mono"],"properties":{"body":{"type":"string"},"heading":{"type":"string"},"mono":{"type":["string","null"]}},"description":"Font references: \"bunny:<Family>:<weights>\", \"custom:<uploaded font id>\" or a system font name."},"radius":{"type":"object","required":["sm","md","lg"],"properties":{"sm":{"type":"string"},"md":{"type":"string"},"lg":{"type":"string"}}},"density":{"type":"string"}}},"TemplateMeta":{"type":"object","required":["title","description","ogImageAssetId","direction","defaultLocale","noindex","keywords","ogTitle","ogDescription","nofollowExternalLinks","nofollowWhitelist"],"properties":{"title":{"type":"string"},"description":{"type":["string","null"]},"ogImageAssetId":{"type":["string","null"]},"direction":{"type":"string"},"defaultLocale":{"type":"string"},"noindex":{"type":"boolean"},"keywords":{"type":["string","null"]},"ogTitle":{"type":["string","null"]},"ogDescription":{"type":["string","null"]},"nofollowExternalLinks":{"type":"boolean"},"nofollowWhitelist":{"type":"array","items":{"type":"string"}}}},"TemplateRegionConfig":{"type":"object","required":["background","padding","borderBottom","align"],"properties":{"background":{"type":["string","null"]},"padding":{"type":["string","null"]},"borderBottom":{"type":"boolean"},"align":{"type":["string","null"]}}},"TemplateComponentInstance":{"type":"object","required":["id","type","config","visibleOn","children"],"properties":{"id":{"type":"string"},"type":{"type":"string","description":"Component type from the registry (see GET /v1/template/schema)."},"config":{"type":"object","description":"The component's settings; keys and values depend on `type` (guide topic `components`)."},"visibleOn":{"type":["object","null"],"properties":{"locales":{"type":["array","null"],"items":{"type":"string"}},"auth":{"type":"string"}}},"children":{"type":"array","items":{"$ref":"#/components/schemas/TemplateComponentInstance"},"description":"Nested components (Hero and Container)."}}},"TemplateRegion":{"type":"object","required":["instances","config"],"properties":{"instances":{"type":"array","items":{"$ref":"#/components/schemas/TemplateComponentInstance"}},"config":{"anyOf":[{"$ref":"#/components/schemas/TemplateRegionConfig"},{"type":"null"}],"description":"Region wrapper styling; null means defaults."}}},"TemplatePage":{"type":"object","required":["header","hero","sidebar","main","footer","utility","layout"],"description":"One page kind: its six regions plus the page settings. Every page carries every page setting (the category page's articleList*/subcategory*/FAQ settings and the search page's searchResult* settings included); only the ones the guide's `settings` topic lists for that page kind take effect.","properties":{"header":{"$ref":"#/components/schemas/TemplateRegion"},"hero":{"$ref":"#/components/schemas/TemplateRegion"},"sidebar":{"$ref":"#/components/schemas/TemplateRegion"},"main":{"$ref":"#/components/schemas/TemplateRegion"},"footer":{"$ref":"#/components/schemas/TemplateRegion"},"utility":{"$ref":"#/components/schemas/TemplateRegion"},"layout":{"type":"string"},"sidebarWidth":{"type":["integer","null"]},"pageWidth":{"type":["integer","null"]},"contentWidth":{"type":["integer","null"]},"columnGap":{"type":["integer","null"]}},"additionalProperties":true},"TemplateNavigationItem":{"type":"object","required":["id","type","label","href","cssClass","children"],"properties":{"id":{"type":"string"},"type":{"type":"string"},"label":{"type":"string"},"href":{"type":"string"},"cssClass":{"type":["string","null"]},"children":{"type":["array","null"],"items":{"$ref":"#/components/schemas/TemplateNavigationItem"}}}},"TemplateScript":{"type":"object","required":["id","position","src","integrity","defer","async","label","inlineBody","pages","enabled","htmlBody"],"properties":{"id":{"type":"string"},"position":{"type":"string"},"src":{"type":["string","null"]},"integrity":{"type":["string","null"]},"defer":{"type":"boolean"},"async":{"type":"boolean"},"label":{"type":["string","null"]},"inlineBody":{"type":["string","null"]},"pages":{"type":["array","null"],"items":{"type":"string"},"description":"null = every page."},"enabled":{"type":"boolean"},"htmlBody":{"type":["string","null"]}}},"TemplateCustomCssItem":{"type":"object","required":["id","label","css","enabled"],"properties":{"id":{"type":"string"},"label":{"type":"string"},"css":{"type":"string"},"enabled":{"type":"boolean"}}},"TemplateDocument":{"type":"object","description":"A v2 template document (schema version 4 at the time of writing). Top-level keys are stable; component `config` contents are registry-driven and documented by GET /v1/template/schema rather than here.","required":["schemaVersion","brand","theme","pages","globals","meta","preset","customCss","customCssItems","syncedRegions"],"properties":{"schemaVersion":{"type":"integer"},"brand":{"$ref":"#/components/schemas/TemplateBrand"},"theme":{"$ref":"#/components/schemas/TemplateTheme"},"pages":{"type":"object","required":["home","category","article","search","contact","notFound"],"properties":{"home":{"$ref":"#/components/schemas/TemplatePage"},"category":{"$ref":"#/components/schemas/TemplatePage"},"article":{"$ref":"#/components/schemas/TemplatePage"},"search":{"$ref":"#/components/schemas/TemplatePage"},"contact":{"$ref":"#/components/schemas/TemplatePage"},"notFound":{"$ref":"#/components/schemas/TemplatePage"}}},"globals":{"type":"object","required":["navigation","scripts"],"properties":{"navigation":{"type":"array","items":{"$ref":"#/components/schemas/TemplateNavigationItem"}},"scripts":{"type":"array","items":{"$ref":"#/components/schemas/TemplateScript"}}}},"meta":{"$ref":"#/components/schemas/TemplateMeta"},"preset":{"type":["string","null"],"description":"The theme preset the document was built from."},"customCss":{"type":"string","description":"Compiled from `customCssItems`; this is what renders."},"customCssItems":{"type":"array","items":{"$ref":"#/components/schemas/TemplateCustomCssItem"}},"syncedRegions":{"type":"array","items":{"type":"string"},"description":"Regions shared by every page (`main` never syncs)."}}},"TemplateOutlineComponent":{"type":"object","required":["type"],"properties":{"id":{"type":"string"},"type":{"type":"string"},"unknownType":{"const":true,"description":"Present only for a type this release no longer supports."},"config":{"type":"object","description":"Only the settings that differ from the type's defaults; long strings and large values are clipped with a note."},"children":{"type":"array","items":{"$ref":"#/components/schemas/TemplateOutlineComponent"}}}},"TemplateOutlineRegion":{"type":["object","array"],"description":"Keys are omitted when empty, and a region with nothing to list is an empty array `[]`.","properties":{"config":{"$ref":"#/components/schemas/TemplateRegionConfig"},"components":{"type":"array","items":{"$ref":"#/components/schemas/TemplateOutlineComponent"}},"styledByCustomCss":{"type":"array","items":{"type":"string"},"description":"Labels of enabled custom CSS blocks that target this region (only when it holds components)."}},"maxItems":0},"TemplateOutline":{"type":"object","required":["brand","theme","meta","presetStartedFrom","syncedRegions","sharedRegions","syncedButDiverged","pages","navigation","scripts","customCss"],"properties":{"brand":{"$ref":"#/components/schemas/TemplateBrand"},"theme":{"$ref":"#/components/schemas/TemplateTheme"},"meta":{"$ref":"#/components/schemas/TemplateMeta"},"presetStartedFrom":{"type":["string","null"]},"syncedRegions":{"type":"array","items":{"type":"string","enum":["header","hero","sidebar","footer","utility"]}},"sharedRegions":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/TemplateOutlineRegion"},"description":"Synced regions whose pages all hold the same content, listed once here and left out of `pages`."},"syncedButDiverged":{"type":["object","null"],"required":["regions","note"],"properties":{"regions":{"type":"array","items":{"type":"string"}},"note":{"type":"string"}},"description":"Synced regions whose pages hold different content (listed per page); edits to them are refused until re-synced or unsynced."},"pages":{"type":"object","required":["home","category","article","search","contact","notFound"],"properties":{"home":{"type":"object","required":["settings","regions"],"properties":{"settings":{"type":"object","description":"`layout` plus the page settings for this page kind that differ from their defaults."},"regions":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/TemplateOutlineRegion"},"description":"Non-empty regions not listed under `sharedRegions`."}}},"category":{"type":"object","required":["settings","regions"],"properties":{"settings":{"type":"object","description":"`layout` plus the page settings for this page kind that differ from their defaults."},"regions":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/TemplateOutlineRegion"},"description":"Non-empty regions not listed under `sharedRegions`."}}},"article":{"type":"object","required":["settings","regions"],"properties":{"settings":{"type":"object","description":"`layout` plus the page settings for this page kind that differ from their defaults."},"regions":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/TemplateOutlineRegion"},"description":"Non-empty regions not listed under `sharedRegions`."}}},"search":{"type":"object","required":["settings","regions"],"properties":{"settings":{"type":"object","description":"`layout` plus the page settings for this page kind that differ from their defaults."},"regions":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/TemplateOutlineRegion"},"description":"Non-empty regions not listed under `sharedRegions`."}}},"contact":{"type":"object","required":["settings","regions"],"properties":{"settings":{"type":"object","description":"`layout` plus the page settings for this page kind that differ from their defaults."},"regions":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/TemplateOutlineRegion"},"description":"Non-empty regions not listed under `sharedRegions`."}}},"notFound":{"type":"object","required":["settings","regions"],"properties":{"settings":{"type":"object","description":"`layout` plus the page settings for this page kind that differ from their defaults."},"regions":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/TemplateOutlineRegion"},"description":"Non-empty regions not listed under `sharedRegions`."}}}}},"navigation":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"type":{"type":"string"},"label":{"type":"string"},"href":{"type":"string"},"cssClass":{"type":"string"},"children":{"type":"array","items":{"type":"object"}}},"description":"Navigation items with null keys omitted."}},"scripts":{"type":"array","items":{"type":"object","required":["kind","enabled"],"properties":{"id":{"type":"string"},"label":{"type":"string"},"position":{"type":"string"},"kind":{"type":"string","enum":["src","inline","html"]},"src":{"type":"string"},"enabled":{"type":"boolean"},"pages":{"type":"array","items":{"type":"string"}},"bytes":{"type":"integer","description":"Size of the inline or HTML body; omitted when empty."}},"description":"Script index without bodies; null keys omitted."}},"customCss":{"type":"array","items":{"type":"object","required":["id","label","enabled","bytes","selectors"],"properties":{"id":{"type":["string","null"]},"label":{"type":"string"},"enabled":{"type":"boolean"},"bytes":{"type":"integer"},"selectors":{"type":"array","maxItems":3,"items":{"type":"string"},"description":"The block's first few selectors."}}}}}},"TemplateGuide":{"type":"object","required":["topic"],"description":"Topic-dependent. overview: summary, workflow, concepts, operations, components (index), site, limits, moreTopics. components: notes + components (full settings of the requested types), or message + components (the index) when `types` is empty or names an unknown type. settings: howToSet, brand, theme, meta, pageSettings, regionStyling. theme: colors, fonts, radius, density, startingPoint and more. css: scoping, hooks, variables, darkMode, contrast, removed, organisation, example. scripts: what, kinds, positions, pages, enabled, safety, notCustomHtml. navigation: what, item, hrefs, replace.","properties":{"topic":{"type":"string","enum":["overview","components","settings","theme","css","scripts","navigation"]},"operations":{"type":"object","description":"overview only: one entry per operation `op`.","additionalProperties":{"type":"object","required":["description","mcp_tool","fields","example"],"properties":{"description":{"type":"string"},"mcp_tool":{"type":"string","description":"The MCP connector (2.0.0 and later) tool that applies this operation, e.g. `helpcenter_update_template_settings` for `set`."},"fields":{"type":"array","items":{"type":"string"}},"example":{"type":"object","description":"A sample operation of this kind."}}}},"components":{"type":"array","items":{"type":"object","required":["type"],"properties":{"type":{"type":"string"},"label":{"type":"string"},"purpose":{"type":"string"},"pages":{"type":"array","items":{"type":"string"}},"regions":{"type":"array","items":{"type":"string"}},"onePerRegion":{"type":"boolean","description":"A region holds at most one of these."},"inline":{"type":"boolean","description":"Renders inline with its neighbours in a header row."},"container":{"type":"boolean","description":"Holds child components."},"acceptsChildren":{"type":"array","items":{"type":"string"},"description":"Component types it may hold. Only on containers."},"defaultChildren":{"type":"array","items":{"type":"object","required":["type"],"properties":{"type":{"type":"string"},"config":{"type":"object"}}},"description":"Children a new instance is seeded with. Only when it has any."},"settings":{"type":["object","string"],"description":"Each setting it accepts, by key; the string `(none — this component has no settings)` when it has none."},"cssHook":{"type":"string","description":"The selector custom CSS can target it by."}},"description":"The index (overview, or the components topic without `types`) carries `onePerRegion`, `inline` and `container` only when true. A full description (the components topic with `types`) always carries them and adds `acceptsChildren`, `defaultChildren`, `settings` and `cssHook`."}},"site":{"type":"object","description":"overview only.","properties":{"defaultLanguage":{"type":"string"},"languages":{"type":"array","items":{"type":"string"}},"aiAnswersEnabled":{"type":"boolean"},"faqSections":{"type":"array","items":{"type":"object","required":["key","name"],"properties":{"key":{"type":"string"},"name":{"type":"string"}}}},"customFonts":{"type":"array","items":{"type":"object","required":["id","name"],"properties":{"id":{"type":"string"},"name":{"type":"string"}}}},"categories":{"type":"string"}}},"limits":{"type":"object","description":"overview only.","properties":{"operationsPerBatch":{"type":"integer"},"customCssBytes":{"type":"integer"},"scriptBodyBytes":{"type":"integer"},"navigationItems":{"type":"integer"}}},"message":{"type":"string","description":"components topic only, when `types` is empty or unknown."}},"additionalProperties":true},"TemplateThemeSummary":{"type":"object","required":["key","name","summary","current"],"description":"A gallery entry. The blank starter (`blank`) carries only key, name, summary and current; every other theme also has tagline, best_for, palette, dark_palette, fonts, density and preview_image, and `industry` when it is an industry theme.","properties":{"key":{"type":"string"},"name":{"type":"string"},"industry":{"type":"string"},"tagline":{"type":"string"},"summary":{"type":"string"},"best_for":{"type":"array","items":{"type":"string"}},"palette":{"type":"object","required":["primary","accent","background","surface"],"properties":{"primary":{"type":"string"},"accent":{"type":"string"},"background":{"type":"string"},"surface":{"type":"string"}}},"dark_palette":{"type":"object","required":["primary","accent","background","surface"],"properties":{"primary":{"type":"string"},"accent":{"type":"string"},"background":{"type":"string"},"surface":{"type":"string"}}},"fonts":{"type":"object","required":["heading","body"],"properties":{"heading":{"type":"string"},"body":{"type":"string"}}},"density":{"type":"string"},"preview_image":{"type":"string","format":"uri"},"current":{"type":"boolean","description":"Whether the draft's preset_key is this theme."}}},"TemplateThemeDetail":{"allOf":[{"$ref":"#/components/schemas/TemplateThemeSummary"},{"type":"object","required":["changed_sections","sections"],"properties":{"screenshots":{"type":"object","properties":{"home":{"type":"string","format":"uri"},"category":{"type":"string","format":"uri"},"article":{"type":"string","format":"uri"},"search":{"type":"string","format":"uri"},"dark":{"type":"string","format":"uri"},"phone":{"type":"string","format":"uri"}},"description":"Catalogue themes only."},"description":{"type":"array","items":{"type":"string"},"description":"Catalogue themes only."},"anatomy":{"type":"object","required":["home","category","article"],"properties":{"home":{"type":"array","items":{"type":"object","required":["label","detail"],"properties":{"label":{"type":"string"},"detail":{"type":["string","null"]}}}},"category":{"type":"array","items":{"type":"object","required":["label","detail"],"properties":{"label":{"type":"string"},"detail":{"type":["string","null"]}}}},"article":{"type":"array","items":{"type":"object","required":["label","detail"],"properties":{"label":{"type":"string"},"detail":{"type":["string","null"]}}}}},"description":"Catalogue themes only: what each page is built from."},"changed_sections":{"type":"array","items":{"type":"string","enum":["brand","theme.colors","theme.darkColors","theme.fonts","theme.radius","theme.density","pages.home","pages.category","pages.article","pages.search","pages.contact","pages.notFound","globals.navigation","globals.scripts","meta","customCss"]},"description":"Sections applying this theme would change on the current draft."},"sections":{"type":"array","items":{"type":"string","enum":["brand","theme.colors","theme.darkColors","theme.fonts","theme.radius","theme.density","pages.home","pages.category","pages.article","pages.search","pages.contact","pages.notFound","globals.navigation","globals.scripts","meta","customCss"]},"description":"Every section key, in order."}}}]},"TemplateNavigationItemInput":{"type":"object","required":["label","href"],"additionalProperties":false,"properties":{"id":{"type":["string","null"],"pattern":"^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$","description":"Omit for a new item (generated). Unique across the whole menu, children included."},"type":{"type":["string","null"],"enum":["link","category","external",null],"description":"Defaults to link."},"label":{"type":"string","minLength":1,"maxLength":100,"description":"Stored trimmed."},"href":{"type":"string","description":"A /path on the help center, an http(s):// URL or mailto:address; at most 2048 bytes, no whitespace, quotes or angle brackets."},"cssClass":{"type":["string","null"],"pattern":"^[A-Za-z0-9_ -]{0,100}$","description":"\"\" is stored as null."},"children":{"type":["array","null"],"maxItems":20,"items":{"$ref":"#/components/schemas/TemplateNavigationItemInput"},"description":"One level only: a child item with non-empty children is refused."}}},"TemplateOperationSet":{"type":"object","description":"Set document settings: brand.*, theme.colors.*, theme.darkColors.*, theme.fonts.*, theme.radius.*, theme.density, meta.* and pages.<page>.<setting> (closed list; guide topic `settings`). A group path with an object value sets one leaf per key. null clears a nullable setting; \"\" is a distinct value (e.g. brand.connectedSiteUrl \"\" clears the field) and is refused where a setting does not allow empty. Colours may be given as rgb(), rgba(), hsl() or hsla() and are stored as hex. An image setting (brand.logoAssetId, brand.logoDarkAssetId, brand.faviconAssetId, meta.ogImageAssetId) also takes {\"source_url\": \"https://…\"}: before the draft is touched, the server downloads the image (PNG, JPEG, GIF, WebP, SVG sanitized, or ICO; at most 5 distinct images a batch), stores it on the help center like POST /v1/images and saves the stored address. A download that fails refuses the batch with 422 `template_invalid`.","required":["op","path","value"],"properties":{"op":{"const":"set"},"path":{"type":"string","minLength":1},"value":{"description":"Any JSON value accepted by the setting(s) at `path`, or {\"source_url\": \"https://…\"} for an image setting."}},"additionalProperties":false},"TemplateOperationAddComponent":{"type":"object","description":"Add a component. At the top level give `page` and `region`; inside a Hero/Container give `parent_id` (page and region come from it). An edit to a synced region is mirrored to every page.","required":["op","type"],"properties":{"op":{"const":"add_component"},"page":{"type":"string","enum":["home","category","article","search","contact","notFound"]},"region":{"type":"string","enum":["header","hero","sidebar","main","footer","utility"]},"type":{"type":"string","minLength":1,"description":"Component type (guide topic `components`)."},"config":{"type":"object","description":"Initial settings; omitted keys keep the type's defaults."},"index":{"type":["integer","null"],"minimum":0,"description":"0-based position; omit or null to append."},"parent_id":{"type":["string","null"],"minLength":1},"id":{"type":["string","null"],"pattern":"^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$","description":"Choose the new item's id (unique; refer to it later in the same batch). Omit or null to have one generated."}},"additionalProperties":false,"anyOf":[{"required":["page","region"]},{"required":["parent_id"],"properties":{"parent_id":{"type":"string"}}}]},"TemplateOperationUpdateComponent":{"type":"object","description":"Merge settings into a component's config; null resets a key to its default. A Hero's backgroundImageUrl also takes {\"source_url\": \"https://…\"}, imported like an image setting of `set`.","required":["op","id","config"],"properties":{"op":{"const":"update_component"},"id":{"type":"string","minLength":1},"page":{"type":["string","null"],"enum":["home","category","article","search","contact","notFound",null],"description":"The page holding the id; needed when the id is on several pages of an unsynced region."},"config":{"type":"object","minProperties":1}},"additionalProperties":false},"TemplateOperationRemoveComponent":{"type":"object","description":"Remove a component and everything nested in it.","required":["op","id"],"properties":{"op":{"const":"remove_component"},"id":{"type":"string","minLength":1},"page":{"type":["string","null"],"enum":["home","category","article","search","contact","notFound",null],"description":"The page holding the id; needed when the id is on several pages of an unsynced region."}},"additionalProperties":false},"TemplateOperationMoveComponent":{"type":"object","description":"Reorder or move a component within its page: to another region, into a container (`parent_id`) or to the top level (`parent_id: null`). Omitting `region` and `parent_id` keeps it where it is, so {id, index} reorders it among its siblings.","required":["op","id"],"properties":{"op":{"const":"move_component"},"id":{"type":"string","minLength":1},"page":{"type":["string","null"],"enum":["home","category","article","search","contact","notFound",null],"description":"The page holding the id; needed when the id is on several pages of an unsynced region."},"region":{"type":"string","enum":["header","hero","sidebar","main","footer","utility"]},"parent_id":{"type":["string","null"],"description":"Container to move into; explicit null lifts to the top level; omitted keeps the current container (unless the region changes)."},"index":{"type":["integer","null"],"minimum":0,"description":"0-based position; omit or null to append."}},"additionalProperties":false},"TemplateOperationUpdateRegion":{"type":"object","description":"Style a region's wrapper. null (or \"\" for background) resets a key; a config left at defaults is stored as null.","required":["op","page","region","config"],"properties":{"op":{"const":"update_region"},"page":{"type":"string","enum":["home","category","article","search","contact","notFound"]},"region":{"type":"string","enum":["header","hero","sidebar","main","footer","utility"]},"config":{"type":"object","minProperties":1,"additionalProperties":false,"properties":{"background":{"type":["string","null"],"description":"Hex colour or a theme token like var(--hc-color-primary)."},"padding":{"type":["string","null"],"enum":["compact","standard","tall",null]},"borderBottom":{"type":"boolean"},"align":{"type":["string","null"],"enum":["left","center",null]}}}},"additionalProperties":false},"TemplateOperationSyncRegion":{"type":"object","description":"Share a region across every page (enabled: true copies `source_page`'s version, translations included, over the others) or let pages differ (enabled: false).","required":["op","region","enabled"],"properties":{"op":{"const":"sync_region"},"region":{"type":"string","enum":["header","hero","sidebar","footer","utility"]},"enabled":{"type":"boolean"},"source_page":{"type":"string","enum":["home","category","article","search","contact","notFound"],"description":"Defaults to home."}},"additionalProperties":false},"TemplateOperationCopyRegion":{"type":"object","description":"One-off copy of an unsynced region, translations included, from one page to others. Refused for a synced region.","required":["op","region","from_page","to_pages"],"properties":{"op":{"const":"copy_region"},"region":{"type":"string","enum":["header","hero","sidebar","footer","utility"]},"from_page":{"type":"string","enum":["home","category","article","search","contact","notFound"]},"to_pages":{"type":"array","minItems":1,"items":{"type":"string","enum":["home","category","article","search","contact","notFound"]}}},"additionalProperties":false},"TemplateOperationAddCss":{"type":"object","description":"Add a labelled custom CSS block (sanitised and compiled into customCss). At most the configured size (204800 bytes by default; guide `limits.customCssBytes`).","required":["op","css"],"properties":{"op":{"const":"add_css"},"id":{"type":["string","null"],"pattern":"^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$","description":"Choose the new item's id (unique; refer to it later in the same batch). Omit or null to have one generated."},"label":{"type":["string","null"],"maxLength":200,"description":"Defaults to \"Custom CSS\"."},"css":{"type":"string","minLength":1},"enabled":{"type":["boolean","null"],"description":"Defaults to true."},"index":{"type":["integer","null"],"minimum":0,"description":"0-based position; omit or null to append."}},"additionalProperties":false},"TemplateOperationUpdateCss":{"type":"object","description":"Change a CSS block's css, label, enabled flag or position. Only the fields sent change.","required":["op","id"],"properties":{"op":{"const":"update_css"},"id":{"type":"string","minLength":1},"label":{"type":["string","null"],"maxLength":200,"description":"null stores an empty label."},"css":{"type":"string","description":"May be empty."},"enabled":{"type":["boolean","null"],"description":"null means true."},"index":{"type":["integer","null"],"minimum":0,"description":"0-based position; omit or null to append."}},"additionalProperties":false},"TemplateOperationRemoveCss":{"type":"object","description":"Delete a CSS block.","required":["op","id"],"properties":{"op":{"const":"remove_css"},"id":{"type":"string","minLength":1}},"additionalProperties":false},"TemplateOperationAddScript":{"type":"object","description":"Inject a script or snippet on the live site: exactly one of `src`, `inlineBody` or `htmlBody`. Defaults: defer true, async false, enabled true, pages null.","required":["op","position"],"properties":{"op":{"const":"add_script"},"id":{"type":["string","null"],"pattern":"^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$","description":"Choose the new item's id (unique; refer to it later in the same batch). Omit or null to have one generated."},"label":{"type":["string","null"],"maxLength":200},"position":{"type":"string","enum":["headStart","headEnd","bodyStart","bodyEnd"]},"src":{"type":"string","description":"External script URL; must start with https:// and contain no whitespace, quotes or angle brackets. At most 51200 bytes."},"inlineBody":{"type":"string","description":"JavaScript source without <script> tags. Non-empty, at most 51200 bytes."},"htmlBody":{"type":"string","description":"Raw HTML such as a vendor snippet or a verification <meta> tag. Non-empty, at most 51200 bytes."},"pages":{"type":["array","null"],"items":{"type":"string","enum":["home","category","article","search","contact","notFound"]},"description":"Page kinds to run on; null or [] means every page."},"enabled":{"type":["boolean","null"]},"defer":{"type":["boolean","null"]},"async":{"type":["boolean","null"]},"integrity":{"type":["string","null"],"pattern":"^sha(256|384|512)-[A-Za-z0-9+/=]+$","description":"Subresource Integrity hash, or null."},"index":{"type":["integer","null"],"minimum":0,"description":"0-based position; omit or null to append."}},"additionalProperties":false,"oneOf":[{"required":["src"]},{"required":["inlineBody"]},{"required":["htmlBody"]}]},"TemplateOperationUpdateScript":{"type":"object","description":"Change any add_script field. Send at most one of `src`, `inlineBody`, `htmlBody`; sending one switches the script to that kind.","required":["op","id"],"properties":{"op":{"const":"update_script"},"id":{"type":"string","minLength":1},"label":{"type":["string","null"],"maxLength":200},"position":{"type":"string","enum":["headStart","headEnd","bodyStart","bodyEnd"]},"src":{"type":"string","description":"External script URL; must start with https:// and contain no whitespace, quotes or angle brackets. At most 51200 bytes."},"inlineBody":{"type":"string","description":"JavaScript source without <script> tags. Non-empty, at most 51200 bytes."},"htmlBody":{"type":"string","description":"Raw HTML such as a vendor snippet or a verification <meta> tag. Non-empty, at most 51200 bytes."},"pages":{"type":["array","null"],"items":{"type":"string","enum":["home","category","article","search","contact","notFound"]},"description":"Page kinds to run on; null or [] means every page."},"enabled":{"type":["boolean","null"]},"defer":{"type":["boolean","null"]},"async":{"type":["boolean","null"]},"integrity":{"type":["string","null"],"pattern":"^sha(256|384|512)-[A-Za-z0-9+/=]+$","description":"Subresource Integrity hash, or null."},"index":{"type":["integer","null"],"minimum":0,"description":"0-based position; omit or null to append."}},"additionalProperties":false},"TemplateOperationRemoveScript":{"type":"object","description":"Delete a script.","required":["op","id"],"properties":{"op":{"const":"remove_script"},"id":{"type":"string","minLength":1}},"additionalProperties":false},"TemplateOperationSetNavigation":{"type":"object","description":"Replace the global menu shared by every Navigation component. Keep existing items' ids to keep their translations; an empty array clears the menu.","required":["op","items"],"properties":{"op":{"const":"set_navigation"},"items":{"type":"array","maxItems":30,"items":{"$ref":"#/components/schemas/TemplateNavigationItemInput"}}},"additionalProperties":false},"TemplateOperation":{"description":"One edit operation, selected by `op`. Unknown fields are refused (422 template_invalid). The guide's overview lists each op's fields.","oneOf":[{"$ref":"#/components/schemas/TemplateOperationSet"},{"$ref":"#/components/schemas/TemplateOperationAddComponent"},{"$ref":"#/components/schemas/TemplateOperationUpdateComponent"},{"$ref":"#/components/schemas/TemplateOperationRemoveComponent"},{"$ref":"#/components/schemas/TemplateOperationMoveComponent"},{"$ref":"#/components/schemas/TemplateOperationUpdateRegion"},{"$ref":"#/components/schemas/TemplateOperationSyncRegion"},{"$ref":"#/components/schemas/TemplateOperationCopyRegion"},{"$ref":"#/components/schemas/TemplateOperationAddCss"},{"$ref":"#/components/schemas/TemplateOperationUpdateCss"},{"$ref":"#/components/schemas/TemplateOperationRemoveCss"},{"$ref":"#/components/schemas/TemplateOperationAddScript"},{"$ref":"#/components/schemas/TemplateOperationUpdateScript"},{"$ref":"#/components/schemas/TemplateOperationRemoveScript"},{"$ref":"#/components/schemas/TemplateOperationSetNavigation"}],"discriminator":{"propertyName":"op","mapping":{"set":"#/components/schemas/TemplateOperationSet","add_component":"#/components/schemas/TemplateOperationAddComponent","update_component":"#/components/schemas/TemplateOperationUpdateComponent","remove_component":"#/components/schemas/TemplateOperationRemoveComponent","move_component":"#/components/schemas/TemplateOperationMoveComponent","update_region":"#/components/schemas/TemplateOperationUpdateRegion","sync_region":"#/components/schemas/TemplateOperationSyncRegion","copy_region":"#/components/schemas/TemplateOperationCopyRegion","add_css":"#/components/schemas/TemplateOperationAddCss","update_css":"#/components/schemas/TemplateOperationUpdateCss","remove_css":"#/components/schemas/TemplateOperationRemoveCss","add_script":"#/components/schemas/TemplateOperationAddScript","update_script":"#/components/schemas/TemplateOperationUpdateScript","remove_script":"#/components/schemas/TemplateOperationRemoveScript","set_navigation":"#/components/schemas/TemplateOperationSetNavigation"}}},"TemplateOperationResult":{"type":"object","required":["operation","op"],"properties":{"operation":{"type":"integer","description":"0-based index in the batch."},"op":{"type":"string","enum":["set","add_component","update_component","remove_component","move_component","update_region","sync_region","copy_region","add_css","update_css","remove_css","add_script","update_script","remove_script","set_navigation"]}},"oneOf":[{"type":"object","required":["paths"],"properties":{"op":{"const":"set"},"paths":{"type":"array","items":{"type":"string"}}}},{"type":"object","required":["id","type","page","region"],"properties":{"op":{"const":"add_component"},"id":{"type":"string"},"type":{"type":"string"},"page":{"type":"string"},"region":{"type":"string"},"parent_id":{"type":"string"},"children":{"type":"array","items":{"type":"object","required":["id","type"],"properties":{"id":{"type":"string"},"type":{"type":"string"}}},"description":"Default children the new component was seeded with (a Hero's SearchBar)."},"mirrored_to_all_pages":{"const":true}}},{"type":"object","required":["id","type","page","region","changed"],"properties":{"op":{"const":"update_component"},"id":{"type":"string"},"type":{"type":"string"},"page":{"type":"string"},"region":{"type":"string"},"changed":{"type":"array","items":{"type":"string"}}}},{"type":"object","required":["id","type","page","region","removed"],"properties":{"op":{"const":"remove_component"},"id":{"type":"string"},"type":{"type":["string","null"]},"page":{"type":"string"},"region":{"type":"string"},"removed":{"type":"array","items":{"type":"string"}}}},{"type":"object","required":["id","page","region"],"properties":{"op":{"const":"move_component"},"id":{"type":"string"},"page":{"type":"string"},"region":{"type":"string"},"parent_id":{"type":"string"}}},{"type":"object","required":["page","region","mirrored_to_all_pages"],"properties":{"op":{"const":"update_region"},"page":{"type":"string"},"region":{"type":"string"},"mirrored_to_all_pages":{"type":"boolean"}}},{"type":"object","required":["region","enabled","changed"],"properties":{"op":{"const":"sync_region"},"region":{"type":"string"},"enabled":{"type":"boolean"},"source_page":{"type":"string"},"changed":{"type":"boolean"}}},{"type":"object","required":["region","from_page","to_pages"],"properties":{"op":{"const":"copy_region"},"region":{"type":"string"},"from_page":{"type":"string"},"to_pages":{"type":"array","items":{"type":"string"}}}},{"type":"object","required":["id","label"],"properties":{"op":{"const":"add_css"},"id":{"type":"string"},"label":{"type":"string"}}},{"type":"object","required":["id","label","enabled"],"properties":{"op":{"const":"update_css"},"id":{"type":"string"},"label":{"type":"string"},"enabled":{"type":"boolean"}}},{"type":"object","required":["id"],"properties":{"op":{"const":"remove_css"},"id":{"type":"string"}}},{"type":"object","required":["id","position","kind"],"properties":{"op":{"const":"add_script"},"id":{"type":"string"},"position":{"type":"string"},"kind":{"type":"string","enum":["src","inline","html"]}}},{"type":"object","required":["id","position","kind","enabled"],"properties":{"op":{"const":"update_script"},"id":{"type":"string"},"position":{"type":"string"},"kind":{"type":"string","enum":["src","inline","html"]},"enabled":{"type":"boolean"}}},{"type":"object","required":["id"],"properties":{"op":{"const":"remove_script"},"id":{"type":"string"}}},{"type":"object","required":["items","ids"],"properties":{"op":{"const":"set_navigation"},"items":{"type":"integer","description":"Top-level item count."},"ids":{"type":"array","items":{"type":"string"}}}}]},"AgentHelpCenterLimits":{"type":"object","description":"What an unclaimed help center may hold and store. The limits lift once it is claimed.","required":["articles","categories","languages","storage_bytes"],"properties":{"articles":{"const":500,"description":"Articles, drafts and articles in Trash included; a language version counts with its article."},"categories":{"const":100,"description":"Categories, those in Trash included."},"languages":{"const":5},"storage_bytes":{"type":"integer","minimum":0,"description":"Bytes of images and files it may store, together: 524288000 (500 MB). Everything stored for it counts: uploads, images `rehost_images` and imports copy, design images given by `source_url`, and the public copy a design makes of one of its private images. An image it already holds is reused and stores nothing."}}},"AgentHelpCenterCreated":{"type":"object","required":["site_id","subdomain","api_key","claim_url","expires_at","limits","docs_url","terms_url"],"properties":{"site_id":{"type":"integer","description":"The help center's id: the `{id}` of its status and discard routes."},"subdomain":{"type":"string","description":"Its temporary address: `agent-` and 8 lowercase letters and digits, on the help-center root domain."},"api_key":{"type":"string","description":"The agent key: send it as the `apikey` header on every other /v1 route. Shown once and stored only as a hash; it expires with the help center. It holds content.read, content.write, design.read and design.write, and cannot change the help center's scripts."},"claim_url":{"type":"string","format":"uri","description":"The link the agent gives its user to claim the help center: `https://helpcenter.io/claim/{token}`. GET /v1/agent/help-centers/{id} shows it again."},"expires_at":{"type":"string","format":"date-time","description":"When the help center expires and becomes subject to deletion unless claimed first: initially 7 days after creation. An open migration request pauses expiry and a new 7-day period starts when the request ends."},"limits":{"$ref":"#/components/schemas/AgentHelpCenterLimits"},"docs_url":{"type":"string","format":"uri","description":"The guide to migrating a knowledge base as an AI agent."},"terms_url":{"type":"string","format":"uri","description":"The Terms of Service covering API and authorised agent use, on the marketing domain."}}},"AgentHelpCenterStatus":{"type":"object","required":["site_id","subdomain","claimed","claim","expires_at","counts","limits","claim_url","preview_url","next_step","setup","capabilities"],"properties":{"site_id":{"type":"integer"},"subdomain":{"type":"string"},"claimed":{"type":"boolean","description":"Whether a person has claimed the help center."},"claim":{"type":"object","required":["state","held_until"],"properties":{"state":{"type":"string","enum":["open","held","claimed"],"description":"`open`: anyone with the claim link can claim it. `held`: someone used the link and it waits for them to confirm their email address; nobody else can claim it until `held_until`. `claimed`: a person has claimed it."},"held_until":{"type":["string","null"],"format":"date-time","description":"While `held`: when the hold lapses, 24 hours after it was placed. From then on anyone with the link can claim it, and the person it waited for still can until someone does. Null otherwise."}}},"expires_at":{"type":["string","null"],"format":"date-time","description":"When an unclaimed help center is deleted; null once it is claimed, and while a migration request is open for it (the deletion clock is paused until the request ends, then restarts at 7 days)."},"counts":{"type":"object","required":["articles","categories","languages","storage_bytes"],"properties":{"articles":{"type":"integer","minimum":0,"description":"Articles, drafts and articles in Trash included."},"categories":{"type":"integer","minimum":0,"description":"Categories, those in Trash included."},"languages":{"type":"integer","minimum":1,"description":"The help center's languages, its default included."},"storage_bytes":{"type":"integer","minimum":0,"description":"Bytes of the images and files it stores, and of any it is storing right now. A write that failed without storage confirming nothing was left counts too, until the claim."}}},"limits":{"$ref":"#/components/schemas/AgentHelpCenterLimits"},"claim_url":{"type":["string","null"],"format":"uri","description":"The claim link while the help center is unclaimed; null once it is claimed."},"preview_url":{"type":["string","null"],"format":"uri","description":"A link that shows the help center as its readers will see it, page to page, for one hour, under a \"Preview: not yet published\" banner: only what an anonymous reader of a public help center may read, published articles alone. A new link on every read of this status. After the claim it follows the same plan policy as publishing: `null` while the help center's owner may not publish it, with `capabilities.preview` naming the `setup` conditions it waits for and `next_step` what is left, and a link again once they may (a help center published once keeps its link). It stops working after the hour, as soon as this key does, or, after the claim, while its owner may not publish the help center, and opens nothing else (no dashboard, no other help center). Read-only, hidden from search engines and never kept by a shared cache. Its pages keep the link's `hc_preview` token in their links, and as a cookie of the help center's host, so a client that keeps no cookies can follow links too. At most 60 page loads a minute per link; past that, `429` with `Retry-After`. A link that has stopped working answers `410`."},"next_step":{"$ref":"#/components/schemas/AgentNextStep","description":"The same as `setup.next_step`, at the top level as on a migration request's status."},"setup":{"$ref":"#/components/schemas/AgentSetup"},"capabilities":{"type":"object","description":"What this key may do on the help center now. HelpCenter.io's plan policy unlocks `redirects` and `custom_domain` for an AI agent once the help center is claimed and its owner has verified their email address and activated a plan; until then a call that needs one answers 403 `capability_locked` (`CapabilityLockedError`). Content and design are the key's from creation. `preview` is the preview link (`preview_url`): enabled while the help center is unclaimed, and after the claim by the same plan policy as publishing, while its owner has verified their email address and activated a plan, or once the help center has been published. While a migration request holds the help center, every write of the key's answers 409 `migration_in_progress` whatever this says (`next_step` `wait_for_migration`). Making the help center public is never one: only its user can.","required":["content","design","redirects","custom_domain","preview"],"properties":{"content":{"$ref":"#/components/schemas/AgentCapability"},"design":{"$ref":"#/components/schemas/AgentCapability"},"redirects":{"$ref":"#/components/schemas/AgentCapability"},"custom_domain":{"$ref":"#/components/schemas/AgentCapability"},"preview":{"$ref":"#/components/schemas/AgentCapability"}}}}},"AgentNextStep":{"type":["object","null"],"description":"What is left, and whose step it is; null once nothing is. One help center's step is the first that applies, with the same code, words and page on either status: `wait_for_migration`: an open migration request holds the help center and is moving the content; read the request's status again after its `retry_after` seconds. `not_claimed`: give the user the claim link (`url`). After the claim the steps are the user's, in their dashboard: `email_unverified`, `plan_not_activated`, then `make_public` until the help center is published. An agent cannot publish a help center. While `redirects`, `custom_domain` or `preview` is locked for an agent's key, its help center's step is never null, and a locked call answers it too (`capability_locked`).","required":["code","instruction","url"],"properties":{"code":{"type":"string","enum":["not_claimed","wait_for_migration","email_unverified","plan_not_activated","make_public"]},"instruction":{"type":"string","description":"The step in words, for the agent to pass on to its user as is."},"url":{"type":["string","null"],"format":"uri","description":"Where the user takes the step: the claim link, their account, the billing page or the help center's settings. Null for `wait_for_migration`."}}},"AgentSetup":{"type":"object","description":"Where the help center's setup stands after the claim. `email_verified` and `plan_activated` describe its owner's account, so both are false until it is claimed.","required":["claimed","email_verified","plan_activated","next_step"],"properties":{"claimed":{"type":"boolean","description":"Whether a person has claimed the help center."},"email_verified":{"type":"boolean","description":"Whether its owner has verified their email address (an account from before HelpCenter.io asked for it counts as verified)."},"plan_activated":{"type":"boolean","description":"Whether its owner has activated a plan for it: a subscription inside its trial or paid, or an active bundle grant for this help center. A free trial with no payment method does not count."},"next_step":{"$ref":"#/components/schemas/AgentNextStep"}}},"AgentCapability":{"type":"object","required":["enabled","requires"],"properties":{"enabled":{"type":"boolean","description":"Whether this key may use it now."},"requires":{"type":"array","description":"The `setup` conditions it still waits for; empty once it is enabled.","items":{"type":"string","enum":["claimed","email_verified","plan_activated"]}}}},"CapabilityLockedError":{"allOf":[{"$ref":"#/components/schemas/Error"}],"description":"An AI agent's key asked for a capability its help center has not unlocked yet: redirects or the custom domain, which HelpCenter.io's plan policy unlocks once the help center is claimed and its owner has verified their email address and activated a plan. `next_step` is the one its status shows.","required":["code","capability","requires","next_step"],"properties":{"code":{"const":"capability_locked"},"capability":{"type":"string","enum":["redirects","custom_domain"]},"requires":{"type":"array","description":"The `setup` conditions still unmet.","items":{"type":"string","enum":["claimed","email_verified","plan_activated"]}},"next_step":{"$ref":"#/components/schemas/AgentNextStep"}}},"LimitReachedError":{"type":"object","required":["status","code","limit","max","message","migration_request_endpoint"],"properties":{"status":{"const":"error"},"code":{"const":"limit_reached"},"limit":{"type":"string","enum":["articles","categories","help_centers_per_hour","help_centers_per_day","storage_bytes"],"description":"Which limit was reached."},"max":{"type":"integer","description":"Its size: a count, or for `storage_bytes` a number of bytes."},"message":{"type":"string"},"migration_request_endpoint":{"const":"POST /v1/agent/migration-requests","description":"Where to file a migration request, the way past every one of these limits."}}},"AgentNotAllowedError":{"allOf":[{"$ref":"#/components/schemas/Error"}],"required":["code"],"properties":{"code":{"const":"agent_not_allowed"}}},"AgentMigrationSource":{"type":"object","required":["url"],"additionalProperties":false,"properties":{"url":{"type":"string","format":"uri","maxLength":2048,"description":"The https address of the knowledge base to migrate. Filing the request reads nothing there. The automatic migration reads it when it is a public Zendesk, Intercom, Document360, Help Scout, Front or HelpDocs help center; otherwise our team does."},"name":{"type":["string","null"],"maxLength":100,"description":"Its name, usually the user's product or company: the name of the help center created for it. A name containing a protected brand name is refused. Without one, the source's host name is used."},"article_count":{"type":["integer","null"],"minimum":0,"maximum":1000000,"description":"About how many articles it holds, for our team to plan by."},"note":{"type":["string","null"],"maxLength":2000,"description":"Anything our team should know about this source."},"agent_key":{"type":["string","null"],"maxLength":255,"description":"The agent key of an unclaimed help center this agent already created for this source (`POST /v1/agent/help-centers`): our team migrates into that one. It must be unexpired, its help center unclaimed and in no other open migration request, and each key may name one source. Without it, a new, empty unclaimed help center is created for the source at once; no agent key is issued for it, and it does not count against the per-address creation limits."},"language":{"type":["string","null"],"default":"en","description":"The source's main language, as a supported locale code (`en`, `fr`, `zh-Hans`...), the same codes `POST /v1/agent/help-centers` takes: the help center created for this source has it as its default language, so the source's main language is migrated into it. Only for a source without `agent_key`, which is refused with one: a help center the agent already created keeps the language it was created with, as does the one `POST /v1/agent/help-centers/{id}/migration-request` files."}}},"AgentMigrationRequestCreated":{"type":"object","required":["request_id","status","expected_by","status_token","help_centers","handoff_url"],"properties":{"request_id":{"type":"integer","description":"The request's id, for `GET /v1/agent/migration-requests/{id}`."},"status":{"const":"queued"},"expected_by":{"type":"string","format":"date-time","description":"When the request is expected to be handled: 24 hours after it was filed."},"status_token":{"type":"string","minLength":40,"description":"The read-only token for `GET /v1/agent/migration-requests/{id}`, sent as `Authorization: Bearer <status_token>`. Shown only here; it is not an agent key and reads nothing else. It works until 30 days after the request ends."},"help_centers":{"type":"array","maxItems":50,"description":"Each source's help center, in the order the sources were sent.","items":{"type":"object","required":["site_id","subdomain"],"properties":{"site_id":{"type":"integer"},"subdomain":{"type":"string"}}}},"handoff_url":{"type":"string","format":"uri","description":"One claim link for all of the request's help centers. Give it to the user: claiming it makes every one of them theirs, in one account."}}},"AgentMigrationRequestStatus":{"type":"object","required":["request_id","status","expected_by","retry_after","help_centers","handoff_url","next_step"],"properties":{"request_id":{"type":"integer"},"status":{"type":"string","enum":["queued","in_progress","completed","with_our_team","declined"],"description":"The request's: `with_our_team` while any help center is (our team has it: a source the automatic migration could not read or finish, or one our team is talking to the user about); else `in_progress` while any is, or while some are done and others still queued; `queued` until one moves; `completed` once every help center not declined is; `declined` once all are. Declined ones never hold the rest back. The request is open, and its help centers take no writes from agent keys (`migration_in_progress`), until every help center is `completed` or `declined`. A help center the automatic migration handed to our team that nobody on it takes up within 7 days is declined, so no request stays open for good."},"expected_by":{"type":["string","null"],"format":"date-time"},"retry_after":{"const":900,"description":"Seconds worth waiting before checking again."},"help_centers":{"type":"array","maxItems":50,"items":{"type":"object","required":["site_id","subdomain","status","articles_migrated","claimed","reason","dashboard_url"],"properties":{"site_id":{"type":"integer"},"subdomain":{"type":["string","null"],"description":"Its address now; null once the help center has been deleted (unclaimed 7 days after the request ended)."},"status":{"type":"string","enum":["queued","in_progress","completed","with_our_team","declined"],"description":"`queued`, then `in_progress` while it is being migrated. `with_our_team`: our team has it, either because the automatic migration could not take or finish it (a source it does not read, one it may not read, or a run that failed) or because our team is talking to the user; `reason` stays null. One the automatic migration handed to our team that nobody on it takes up within 7 days is declined, with a `reason` that starts `no_staff_action:`. `completed` or `declined` (with a `reason`) once it ends."},"articles_migrated":{"type":"integer","minimum":0},"claimed":{"type":"boolean"},"reason":{"type":["string","null"],"description":"Why it was declined; null otherwise, `with_our_team` included. It starts `no_staff_action:` for a help center our team did not take up within 7 days of the automatic migration handing it to them."},"dashboard_url":{"type":["string","null"],"format":"uri","description":"Its dashboard, once it is claimed; null before."}}}},"handoff_url":{"type":["string","null"],"format":"uri","description":"The one claim link for all of the request's help centers while any of them can still be claimed; once all are claimed, the first one's dashboard; null when none is left."},"next_step":{"$ref":"#/components/schemas/AgentNextStep"}}},"MigrationRequestLimitError":{"type":"object","required":["status","code","limit","max","message"],"properties":{"status":{"const":"error"},"code":{"const":"limit_reached"},"limit":{"const":"migration_requests_per_day"},"max":{"type":"integer"},"message":{"type":"string"}}},"CustomDomain":{"type":"object","description":"Where the help center's custom domain stands. The checks are the dashboard's: the domain's CNAME points at this help center, its certificate is issued, and the certificate serves the domain. `live` needs all three, and needs the domain to reach this help center rather than another one that held it first. They are rerun at most once a minute; `last_checked_at` says when. With no custom domain set, every field but `dns_records` is null.","required":["domain","status","dns_records","certificate","checks","failure","last_checked_at"],"properties":{"domain":{"type":["string","null"],"description":"The custom domain, as stored: without a scheme or a leading `www.`."},"status":{"type":["string","null"],"enum":["pending_dns","verifying","issuing_certificate","live","failed",null],"description":"`pending_dns`: the DNS record in `dns_records` is missing or points elsewhere; the user must add it at their DNS provider. `verifying`: it points here, and HelpCenter.io's next check (every few minutes) picks it up. `issuing_certificate`: the certificate is being requested, issued or attached, which takes minutes. `live`: the help center answers on the domain over HTTPS. `failed`: see `failure`."},"dns_records":{"type":"array","description":"The exact records the user must add at their DNS provider, and keep.","items":{"type":"object","required":["type","name","value"],"properties":{"type":{"const":"CNAME"},"name":{"type":"string","description":"The host the record is for: the custom domain."},"value":{"type":"string","description":"Its target: the help center's own address."}}}},"certificate":{"type":["object","null"],"required":["status"],"properties":{"status":{"type":"string","enum":["not_requested","waiting_for_dns","requested","issued","failed"],"description":"The HTTPS certificate for the domain. `waiting_for_dns`: it is requested once the DNS record points here."}}},"checks":{"type":["object","null"],"required":["dns_points_here","certificate_issued","certificate_serving"],"properties":{"dns_points_here":{"type":"boolean"},"certificate_issued":{"type":"boolean"},"certificate_serving":{"type":"boolean"}}},"failure":{"type":["object","null"],"description":"Why the status is `failed`; null otherwise.","required":["code","message"],"properties":{"code":{"type":"string","enum":["domain_in_use","not_requested","certificate_failed"],"description":"`domain_in_use`: another help center held the domain first and is served on it; contact support if it is the user's. `not_requested`: no certificate was requested, because the domain was saved past the month's limit of changes. `certificate_failed`: the certificate could not be issued."},"message":{"type":"string"}}},"last_checked_at":{"type":["string","null"],"format":"date-time"}}},"CustomDomainResponse":{"type":"object","required":["status","custom_domain"],"properties":{"status":{"const":"success"},"custom_domain":{"$ref":"#/components/schemas/CustomDomain"}}},"CustomDomainNotInPlanError":{"allOf":[{"$ref":"#/components/schemas/Error"}],"required":["code","upgrade_url"],"properties":{"code":{"const":"custom_domain_not_in_plan"},"upgrade_url":{"type":"string","format":"uri","description":"The billing page, where the help center's owner can change plan."}}},"CustomDomainLimitReachedError":{"allOf":[{"$ref":"#/components/schemas/Error"}],"required":["code"],"properties":{"code":{"const":"custom_domain_limit_reached"}}},"ContentImport":{"type":"object","required":["id","platform","importable","source_url","source_host","status","agent_initiated","plan","selection","refusal","limit","progress","imported_articles","needs_review","error","started_at","finished_at","created_at","updated_at"],"properties":{"id":{"type":"integer"},"platform":{"type":"string","enum":["zendesk","front","helpdocs","intercom","document360","helpscout","other"],"description":"`other`: not a platform the importer reads by itself."},"importable":{"type":"boolean","description":"Whether the importer reads this source by itself."},"source_url":{"type":"string"},"source_host":{"type":"string"},"status":{"type":"string","enum":["scanned","scheduled","queued","running","completed","failed","cancelled","assisted"],"description":"`scanned`: not run yet. `scheduled`: set to run later in the dashboard. `queued` and `running`: a run is under way. `completed`, `failed` (see `error`), `cancelled`, or `assisted` (handed to our migration team from the dashboard)."},"agent_initiated":{"type":"boolean","description":"Whether an AI agent's key set it up last."},"plan":{"type":["object","null"],"description":"What the scan found; null when the importer cannot read the source.","required":["articles","categories","languages","default_language","images"],"properties":{"articles":{"type":"integer","description":"Articles in the main language."},"categories":{"type":"array","description":"The top-level categories, each counting everything nested under it.","items":{"type":"object","required":["id","name","articles"],"properties":{"id":{"type":"string"},"name":{"type":"string"},"articles":{"type":"integer"}}}},"languages":{"type":"array","items":{"type":"object","required":["code","label","articles","lands_in"],"properties":{"code":{"type":"string","description":"The source's code for it."},"label":{"type":"string"},"articles":{"type":"integer"},"lands_in":{"type":["string","null"],"description":"The help center language it is imported into, added if the help center lacks it; null when it lands nowhere here (and cannot be chosen)."}}}},"default_language":{"type":"string","description":"The source's main language, which lands in the help center's main language."},"images":{"type":"integer","description":"Images in the articles, estimated for a large help center."}}},"selection":{"type":"object","required":["category_ids","languages","arrive_as","articles"],"properties":{"category_ids":{"type":"array","items":{"type":"string"}},"languages":{"type":"array","items":{"type":"string"}},"arrive_as":{"type":"string","enum":["draft","as_is"]},"articles":{"type":"integer","description":"Articles in the chosen categories, by the scan."}}},"refusal":{"type":["string","null"],"description":"Why a run would write nothing now (the help center's main language changed since the import wrote into it), or null."},"limit":{"type":["object","null"],"description":"For an AI agent's import into an unclaimed help center: its 500 articles, the room left beside every article there (drafts and Trash included) and other imports under way, and what this import still brings. Null for every other import.","required":["max","room","articles_to_import","fits"],"properties":{"max":{"type":"integer"},"room":{"type":"integer"},"articles_to_import":{"type":"integer"},"fits":{"type":"boolean"}}},"progress":{"type":["object","null"],"description":"The latest run, or all of its runs together when a large import goes on in several; null before the first.","required":["total","done","created","updated","conflicts","removed","images","image_failures"],"properties":{"total":{"type":"integer","description":"Articles chosen, by the scan."},"done":{"type":"integer"},"created":{"type":"integer"},"updated":{"type":"integer"},"conflicts":{"type":"integer","description":"Articles changed both at the source and here: kept as they are here, and flagged for review."},"removed":{"type":"integer","description":"Articles gone from the source: flagged for review here, never deleted."},"images":{"type":"integer"},"image_failures":{"type":"integer"}}},"imported_articles":{"type":"integer","description":"Articles this import brought in that are still here."},"needs_review":{"type":"object","required":["count","articles"],"description":"Imported articles flagged for review: a conflict, an article gone from the source, one that arrived private because the source restricts it, or one the source asked to check. The first 50 are listed.","properties":{"count":{"type":"integer"},"articles":{"type":"array","items":{"type":"object","required":["id","title","note"],"properties":{"id":{"type":"integer"},"title":{"type":"string"},"note":{"type":["string","null"]}}}}}},"error":{"type":["object","null"],"required":["message","reason"],"properties":{"message":{"type":"string"},"reason":{"type":["string","null"],"description":"Why the run stopped: `limit_reached` (the 500 articles, or the 500 MB of images and files, `storage_bytes`, an unclaimed help center may hold), `main_language_changed` or `too_large`; or why the source could not be read, with the codes of a scan's `422` (`ContentImportScanError`), `needs_credentials` and `robots` among them, or `not_on_platform` when its address no longer points at its platform. Null when it stopped for a reason of ours."}}},"started_at":{"type":["string","null"],"format":"date-time"},"finished_at":{"type":["string","null"],"format":"date-time"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"ContentImportResponse":{"type":"object","required":["status","import"],"properties":{"status":{"const":"success"},"import":{"$ref":"#/components/schemas/ContentImport"}}},"ContentImportList":{"type":"object","required":["status","imports"],"properties":{"status":{"const":"success"},"imports":{"type":"array","items":{"$ref":"#/components/schemas/ContentImport"}}}},"ContentImportScanError":{"type":"object","required":["status","code","message"],"properties":{"status":{"const":"error"},"code":{"type":"string","description":"`credentials_not_accepted`, or why the scan could not read the address: `needs_credentials`, `not_public`, `robots` (its robots.txt keeps us out, or cannot be read), `too_large`, `remote_error`, `unreachable`, `transport`, `bad_redirect`, `too_many_redirects`, `unresolvable`, `unparseable_url`, `credentials_in_url`, `scheme_not_allowed`, `port_not_allowed`, `ip_in_blocked_range` or `resolves_to_blocked_range`; or, for now, `host_busy` (other help centers are importing from that site at the moment: try again in a few minutes) or `host_daily_limit` (that site has had as many requests from the importer today as one site gets in a day: try again after 00:00 UTC, or file a migration request)."},"platform":{"type":["string","null"],"description":"The platform the address turned out to be, once known: whose credentials `needs_credentials` asks for."},"message":{"type":"string"}}},"ContentImportLimitReachedError":{"allOf":[{"$ref":"#/components/schemas/LimitReachedError"}],"type":"object","required":["room","articles_to_import"],"properties":{"room":{"type":"integer","description":"Articles the help center still has room for, beside other imports under way."},"articles_to_import":{"type":"integer","description":"Articles this import still brings, as chosen: those its scan found in the chosen categories, less those it already imported from them."}}},"Redirect":{"type":"object","required":["id","from","to","created_at","updated_at"],"properties":{"id":{"type":"integer"},"from":{"type":"string","description":"The path it answers, with a leading `/`."},"to":{"type":"string","description":"Where it sends the visitor: a path of the help center, or another address."},"created_at":{"type":["string","null"],"format":"date-time"},"updated_at":{"type":["string","null"],"format":"date-time"}}},"RedirectInput":{"type":"object","required":["from","to"],"properties":{"from":{"type":"string","maxLength":191,"description":"A path of the help center, like `/hc/en-us/articles/360001-Billing`: no scheme or host, no query or fragment (a redirect matches the path alone), and not the home page. At most 191 characters once percent-encoded."},"to":{"type":"string","maxLength":191,"description":"A path starting with `/`, like `/en/content/billing`, or an http(s) URL. For an AI agent's key, a page of this help center."}}},"RedirectSaved":{"type":"object","required":["status","action","redirect"],"properties":{"status":{"const":"success"},"action":{"type":"string","enum":["created","updated"]},"redirect":{"$ref":"#/components/schemas/Redirect"}}},"RedirectRefusedError":{"allOf":[{"$ref":"#/components/schemas/Error"}],"required":["code"],"properties":{"code":{"type":"string","enum":["source_not_redirectable","target_not_found","external_target_forbidden","redirect_loop","too_many_hops"]}}},"ApiKeyRequiredError":{"allOf":[{"$ref":"#/components/schemas/Error"}],"description":"An OAuth token on a route that takes an API key of the help center: no scope a user can consent to covers setting a help center up.","required":["code"],"properties":{"code":{"const":"api_key_required"}}},"RedirectBulkItemResult":{"description":"One item's outcome in `POST /v1/redirects/bulk`.","oneOf":[{"type":"object","required":["index","status","redirect"],"properties":{"index":{"type":"integer","description":"The item's position in `redirects`."},"status":{"type":"string","enum":["created","updated"]},"redirect":{"$ref":"#/components/schemas/Redirect"}}},{"type":"object","required":["index","status","error"],"properties":{"index":{"type":"integer","description":"The item's position in `redirects`."},"status":{"const":"error"},"error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string","enum":["validation_error","source_not_redirectable","target_not_found","external_target_forbidden","redirect_loop","too_many_hops"],"description":"`validation_error`: the item is not an object with a valid `from` and `to` (`errors` holds the field messages). The others as on `POST /v1/redirects`."},"message":{"type":"string"},"errors":{"type":"object","additionalProperties":{"type":"array","items":{"type":"string"}}}}}}}]},"WidgetInstall":{"type":"object","required":["app_id","install_snippet","script_url","embedding_origins","embedding_origins_editable","frame_ancestors","installed","last_seen_at","install_window_days","visitors","entitlements"],"properties":{"app_id":{"type":"string","description":"The widget's id, `hcOptions.app_id` in the snippet. Not a secret: it is in every page that loads the widget."},"install_snippet":{"type":"string","description":"The code to paste before `</body>` on every page of the customer's website that should show the widget, byte for byte what the dashboard's Embeddables page shows. FAQ sections need it on the page too."},"script_url":{"type":"string","format":"uri","description":"Where the snippet loads the widget from."},"embedding_origins":{"type":"array","items":{"type":"string"},"description":"The host names allowed to embed the help center and run its widget, as in effect now. A leading dot (`.example.com`) is the domain and all its subdomains. The widget answers only on pages whose host is listed here: add the customer's website before installing the snippet."},"embedding_origins_editable":{"type":"boolean","description":"Whether this credential may change the list: it holds `embed.manage` and, for an OAuth token, its person is an Owner or Admin."},"frame_ancestors":{"type":"string","description":"The `frame-ancestors` directive help-center pages answer with, from the list: `frame-ancestors 'none'` when it is empty."},"installed":{"type":"boolean","description":"Whether the widget has been seen running on a page within the last `install_window_days`."},"last_seen_at":{"type":["string","null"],"format":"date-time","description":"When the widget last reported from a page; null if never."},"install_window_days":{"type":"integer","minimum":1},"visitors":{"type":"object","required":["served","reason","message"],"properties":{"served":{"type":"boolean","description":"Whether the widget answers anonymous visitors. A correct install still shows them nothing while this is false."},"reason":{"enum":[null,"suspended","unpublished","signed_in_readers_only","ip_restricted"],"description":"`unpublished`: the help center is public but not published yet, so only its team, signed in, is served. `signed_in_readers_only`: it is private or password protected, so only readers signed in to it are. `ip_restricted`: only visitors on its listed networks are. `suspended`: nobody is."},"message":{"type":["string","null"]}}},"entitlements":{"type":"object","required":["widget","ai_answers","proactive_messages"],"properties":{"widget":{"type":"boolean","description":"Whether the plan includes the widget and FAQ sections. FAQ-section writes answer `403 widget_not_in_plan` without it."},"ai_answers":{"type":"boolean","description":"Whether the widget can answer questions with AI."},"proactive_messages":{"type":"boolean","description":"Whether the plan includes proactive messages, which stay in the dashboard."}}}}},"WidgetResponse":{"type":"object","required":["status","widget"],"properties":{"status":{"const":"success"},"widget":{"$ref":"#/components/schemas/WidgetInstall"}}},"EmbeddingOriginsResponse":{"type":"object","required":["status","message","embedding_origins","added","removed","not_listed","frame_ancestors"],"properties":{"status":{"const":"success"},"message":{"type":"string"},"embedding_origins":{"type":"array","items":{"type":"string"},"description":"The whole list, as stored."},"added":{"type":"array","items":{"type":"string"},"description":"Entries the list did not have before."},"removed":{"type":"array","items":{"type":"string"},"description":"Entries it no longer has."},"not_listed":{"type":"array","items":{"type":"string"},"description":"Entries in `remove` the list did not hold."},"frame_ancestors":{"type":"string","description":"The directive help-center pages answer with from now, within minutes as caches refresh."}}},"FaqSectionText":{"type":"object","description":"Text by language: the help center's default language under `default`, others under their language code.","required":["default"],"additionalProperties":{"type":"string"},"examples":[{"default":"Billing questions","fr":"Questions de facturation"}]},"FaqSectionTextInput":{"anyOf":[{"type":"string","description":"Text in the help center's default language."},{"type":"object","additionalProperties":{"type":"string"},"description":"Text by language: `default` (or the default language's own code) and any other language of the help center."}]},"FaqSectionDesign":{"type":"object","description":"The design tokens a template reads. Any left out take the template's own.","additionalProperties":false,"properties":{"accent":{"type":["string","null"],"pattern":"^#[0-9a-fA-F]{6}$"},"radius":{"type":"integer","minimum":0,"maximum":24},"iconStyle":{"enum":["chevron","plus","arrow","none"]},"headFont":{"enum":["auto","system","brand","serif","page"],"description":"`page` inherits the customer's page font."},"theme":{"enum":["light","dark","auto"],"description":"`auto` follows the visitor's color scheme."},"animate":{"type":"boolean"},"groupByCategory":{"type":["boolean","null"]},"contactUrl":{"type":"string","maxLength":2048,"description":"An `https://`, `http://` or `mailto:` link, for the templates with a contact card."}}},"FaqSectionArticle":{"type":"object","required":["id","title","status","type","position","shown"],"properties":{"id":{"type":"integer"},"title":{"type":"string","description":"In the help center's default language."},"status":{"type":"string"},"type":{"type":"string"},"position":{"type":"integer","minimum":0},"shown":{"type":"boolean","description":"Whether visitors see it: only published FAQ articles they may read show, so a draft or private one stays on the list unseen."}}},"FaqSection":{"type":"object","required":["id","key","type","title","description","display_urls","template","design","show_heading","layout","has_custom_css","article_count","install_snippet","created_at","updated_at"],"properties":{"id":{"type":"integer"},"key":{"type":"string","description":"The id the snippet carries (`data-id`)."},"type":{"enum":["full","custom"],"description":"`full` lists every published FAQ article, grouped by category. `custom` lists the FAQs added to it, in its own order."},"title":{"$ref":"#/components/schemas/FaqSectionText"},"description":{"anyOf":[{"$ref":"#/components/schemas/FaqSectionText"},{"type":"null"}]},"display_urls":{"type":"array","items":{"type":"string"},"description":"Host names of pages allowed to load the section. A host matches only itself (www.example.com and example.com are two), and a leading-dot entry (`.example.com`) matches the domain and all its subdomains. The help center's own domain always is."},"template":{"enum":["classic","slate","glass","bubble","ledger","duet","pivot","atlas","haiku","trail","beacon","blok","prism","terminal","marquee","orbit","mosaic","ticket","finder","folio","neon","rail","weave","cove","postcard","bulletin","dialogue","canopy"],"description":"`classic` is the renderer sections used before templates existed."},"design":{"$ref":"#/components/schemas/FaqSectionDesign"},"show_heading":{"type":["boolean","null"]},"layout":{"enum":["default","sequential",null],"description":"The classic renderer's layout."},"has_custom_css":{"type":"boolean","description":"Whether the section carries custom CSS, which only the dashboard edits."},"article_count":{"type":"integer","minimum":0,"description":"How many articles a custom section lists, shown or not."},"install_snippet":{"type":"string","description":"The element to place where the section should appear, on a page that also carries the widget snippet and whose host is in `display_urls`."},"created_at":{"type":["string","null"],"format":"date-time"},"updated_at":{"type":["string","null"],"format":"date-time"},"articles":{"anyOf":[{"type":"array","items":{"$ref":"#/components/schemas/FaqSectionArticle"}},{"type":"null"}],"description":"On a single section: a custom section's list in reading order; null for a full section."}}},"FaqSectionTemplates":{"type":"object","required":["ids","default","classic","design"],"properties":{"ids":{"type":"array","items":{"type":"string"},"description":"Every template a section may take, `classic` first."},"default":{"type":"string","description":"The template a new section starts on."},"classic":{"type":"string"},"design":{"type":"object","additionalProperties":true,"description":"The design tokens and the values each takes."}}},"FaqSectionListResponse":{"type":"object","required":["status","faq_sections","templates"],"properties":{"status":{"const":"success"},"faq_sections":{"type":"array","items":{"$ref":"#/components/schemas/FaqSection"}},"templates":{"$ref":"#/components/schemas/FaqSectionTemplates"}}},"FaqSectionResponse":{"type":"object","required":["status","faq_section"],"properties":{"status":{"const":"success"},"message":{"type":"string"},"faq_section":{"$ref":"#/components/schemas/FaqSection"}}},"FaqSectionArticlesAddedResponse":{"type":"object","required":["status","message","added_article_ids","already_listed_article_ids","faq_section"],"properties":{"status":{"const":"success"},"message":{"type":"string"},"added_article_ids":{"type":"array","items":{"type":"integer"}},"already_listed_article_ids":{"type":"array","items":{"type":"integer"}},"faq_section":{"$ref":"#/components/schemas/FaqSection"}}},"FaqSectionArticleRemovedResponse":{"type":"object","required":["status","message","removed","faq_section"],"properties":{"status":{"const":"success"},"message":{"type":"string"},"removed":{"type":"boolean","description":"False when the section did not list the article; nothing changed then."},"faq_section":{"$ref":"#/components/schemas/FaqSection"}}},"FaqSectionDeletedResponse":{"type":"object","required":["status","message"],"properties":{"status":{"const":"success"},"message":{"type":"string"}}},"FaqSectionArticlesRejectedError":{"allOf":[{"$ref":"#/components/schemas/ValidationError"}],"required":["unknown_article_ids","not_faq_article_ids"],"properties":{"unknown_article_ids":{"type":"array","items":{"type":"integer"},"description":"Not articles of this help center (the same answer whether or not another help center has one)."},"not_faq_article_ids":{"type":"array","items":{"type":"integer"},"description":"Articles of another type than `faq`, which a section never shows."}}},"FaqSectionOrderRejectedError":{"allOf":[{"$ref":"#/components/schemas/ValidationError"}],"required":["missing_article_ids","unlisted_article_ids","repeated_article_ids"],"properties":{"missing_article_ids":{"type":"array","items":{"type":"integer"}},"unlisted_article_ids":{"type":"array","items":{"type":"integer"}},"repeated_article_ids":{"type":"array","items":{"type":"integer"}}}},"WidgetNotInPlanError":{"allOf":[{"$ref":"#/components/schemas/Error"}],"required":["code","upgrade_url"],"properties":{"code":{"const":"widget_not_in_plan"},"upgrade_url":{"type":"string","format":"uri","description":"The billing page, where the help center's owner can change plan."}}},"NoteRefusedError":{"type":"object","description":"A note the request asked for that cannot be placed, so nothing was written (docs/PUBLIC_API_NOTES.md).","required":["status","code","message","field"],"properties":{"status":{"const":"error"},"code":{"enum":["note_quote_not_found","note_quote_ambiguous","note_lang_required","note_locale_not_written","note_quotes_overlap","annotate_changes_covered"],"description":"`note_quote_not_found`: the quoted passage is not in the text; `note_quote_ambiguous`: it occurs more than once (`occurrences`); `note_lang_required`: a write reaching several languages carries a note without `lang`; `note_locale_not_written`: a note for a language the write does not reach (`notes_endpoint`); `note_quotes_overlap`: two notes of one write quote overlapping text (`overlaps` names the other quote), and a passage carries one note's highlight; `annotate_changes_covered`: the write's notes quote every change it makes to a language (`lang`, `covered_by`), so `annotate_changes` would have no passage of its own."},"message":{"type":"string"},"field":{"type":"string","description":"The input the refusal is about: `quote`, or `notes.{n}.quote` / `notes.{n}.lang` / `annotate_changes` on a write."},"occurrences":{"type":"integer","minimum":2},"notes_endpoint":{"type":"string","description":"`/v1/articles/{id}/notes`, for a passage already in the article."},"overlaps":{"type":"string","description":"`note_quotes_overlap`: the field of the earlier note whose quote this one overlaps."},"lang":{"type":"string","description":"`annotate_changes_covered`: the language whose changes the notes cover."},"covered_by":{"type":"array","items":{"type":"string"},"description":"`annotate_changes_covered`: the fields of the notes whose quotes take every change."}}},"ArticleWriteNoteFields":{"type":"object","description":"Team notes on the text this write stores (docs/PUBLIC_API_NOTES.md, \"Notes on a content write\"). Either field needs the `notes.write` scope and the notes permission on top of what the write needs. All or nothing: a refused quote writes nothing.","properties":{"notes":{"type":["array","null"],"maxItems":20,"description":"A note on each quoted passage of the NEW text. The quote must occur exactly once in its language's new text as stored (Markdown converted, sanitized, highlights kept).","items":{"type":"object","required":["quote","note"],"additionalProperties":false,"properties":{"quote":{"type":"string","maxLength":5000,"description":"Plain text exactly as it reads in the article (not Markdown or HTML), long enough to be unique. Matched on its non-whitespace characters."},"note":{"type":"string","minLength":1,"maxLength":5000,"description":"Plain text."},"lang":{"type":["string","null"],"description":"The language whose new text holds the passage. May be left out only when the request writes exactly one language's content."}}}},"annotate_changes":{"type":["object","null"],"required":["note"],"additionalProperties":false,"description":"One note per language whose content this request changes, highlighting every added block and, in each edited block, its first changed word to its last. \"Changed\" is measured against the copy this write replaces (live; for `/staged` the previous staged text, or live on the first staged save).","properties":{"note":{"type":"string","minLength":1,"maxLength":5000}}}}},"CreatedArticleNotes":{"type":"array","description":"Added to a write's answer when it carried `notes` or `annotate_changes`: every note the request created, as a thread.","items":{"allOf":[{"$ref":"#/components/schemas/ArticleNote"},{"required":["replies"]}]}},"ArticleChangeAnnotations":{"type":"array","description":"Added to a write's answer when it carried `annotate_changes`: one entry per language the request's content reached.","items":{"type":"object","required":["lang","status"],"properties":{"lang":{"type":"string"},"status":{"enum":["annotated","skipped"]},"note_id":{"type":"integer"},"highlighted":{"type":"boolean"},"passages":{"type":"integer","description":"Changed stretches highlighted."},"unplaced_passages":{"type":"integer","description":"Changed stretches left unhighlighted because they could not be pinned to exactly one place."},"reason":{"enum":["unchanged","nothing_to_highlight","not_located","too_long"],"description":"`unchanged` (skipped); on an unhighlighted note: `nothing_to_highlight` (removed or text-less changes), `not_located`, `too_long` (too large to compare word by word)."}}}}}}}