Getting started

Requests and responses

Every request to the HelpCenter.io API follows the same rules: one base URL, a few headers, JSON in and out, and the same conventions for languages, dates and content. This page covers them once, so each reference page can stay short.

Base URL

Send every request to https://api.helpcenter.io/v1. Paths in these docs, such as /articles, are relative to it. The version is part of the path.

Use HTTPS. A request to http:// gets a 301 redirect, and many HTTP clients follow a redirect with a GET, dropping the method and the body of your request.

To check that a key works, call the base URL itself. Any valid key or token gets this answer, whatever its scope:

curl https://api.helpcenter.io/v1 \
  -H "apikey: $HELPCENTER_API_KEY" \
  -H "Accept: application/json"

The response:

{
  "status": "success",
  "message": "This is the public API of HelpCenter.io"
}

Without a valid key, the same request answers 401 Unauthorized:

{
  "status": "unauthorized",
  "docs_url": "https://developers.helpcenter.io/content/quickstart-your-first-request",
  "create_url": "https://api.helpcenter.io/v1/agent/help-centers",
  "agent_docs_url": "https://developers.helpcenter.io/content/migrate-a-knowledge-base-as-an-ai-agent"
}

Branch on status. The other fields are links for whoever reads the response: docs_url to the quickstart, and create_url and agent_docs_url for an AI agent with no account, which can create a help center without one (see Migrate a knowledge base as an AI agent).

Headers

Header

Send it

What it does

apikey: <key>

With an API key

Authenticates the request. The key decides which help center you work on. See API keys.

Authorization: Bearer <token>

With an OAuth 2.1 access token

Authenticates the request as the person who connected your app. See OAuth 2.1 for apps and AI clients. If a request carries both headers, the API uses apikey.

X-HCio-Site: <id>

With an OAuth token that reaches more than one help center

Names the help center to act on. The site query parameter does the same. API keys ignore both. See Choose which help center a request acts on.

Accept: application/json

Always

Asks for JSON. The API answers in JSON, errors included. See the next section.

Content-Type: application/json

With a JSON body

Tells the API to read the body as JSON. See Request bodies below.

Header names are not case-sensitive, so ApiKey works too. api_key and X-API-Key don't: the API reads an API key only from the apikey header.

Errors are always JSON

The API answers every error in JSON, with or without Accept: application/json: an invalid value, an unknown path or id, a wrong method, a rate limit and a server error. It never answers with a redirect, so a client that follows redirects can't mistake a refusal for a success. Send the header anyway, as the examples on this site do. Only the web servers in front of the API answer otherwise, when they refuse a request before it reaches the API: a body over 32 MB, or more requests than they allow from one address (see Rate limits). So check the status code before you parse a body.

For example, "published": "yes" isn't a valid value. PATCH /v1/articles/{id} answers 422 Unprocessable Entity:

{
  "status": "validation_error",
  "message": "The request could not be accepted.",
  "errors": {
    "published": ["The published field must be true or false."]
  }
}

An unknown path answers 404 with {"status": "error", "message": "Not found."}, and an unknown article or category id answers 404 with Article not found. or Category not found. Check the status code first, then status or code. Errors lists every case.

Request bodies

Send request bodies as JSON, with Content-Type: application/json. Without that header the API doesn't read the body as JSON, so a complete request fails as if it were empty: POST /v1/articles answers 400 with {"title":["The title field is required."]}.

Image files are the exception: they go up as multipart/form-data. See Images. Parameters of GET and DELETE requests go in the query string.

Responses

A successful response is a JSON object with "status": "success". A single resource comes back under its own name, such as article, category or image. A list comes back under the plural name, with a meta block for paging:

{
  "status": "success",
  "categories": [
    {
      "id": 293,
      "parent": null,
      "name": {"de": "Erste Schritte", "en": "Getting started"},
      "description": {
        "en": "Set up your account and invite your team.",
        "de": "Konto einrichten und das Team einladen."
      },
      "icon": "fa://fa-solid fa-rocket",
      "position": 0,
      "privacy": "public",
      "created_at": "2026-09-30 08:03:09",
      "updated_at": "2026-09-30 08:19:36"
    },
    {
      "id": 316,
      "parent": null,
      "name": {"de": "Abrechnung", "en": "Billing"},
      "description": [],
      "icon": "fa://fa-solid fa-credit-card",
      "position": 1,
      "privacy": "public",
      "created_at": "2026-09-30 08:19:36",
      "updated_at": "2026-09-30 08:19:36"
    }
  ],
  "meta": {"page": 1, "per_page": 2, "total_pages": 2, "items_count": 3}
}

Status

When

200 OK

Reads, updates and deletes. Also a POST /v1/articles that updated an existing article through its external_id: the body says "action": "updated".

201 Created

Something was created: an article ("action": "created"), a category, a comment, a webhook, a change set.

202 Accepted

Publishing a change set. The work runs in the background, and status is queued, or scheduled for a later time.

207 Multi-Status

Bulk requests (POST /v1/articles/bulk, POST /v1/images/bulk). Every item has its own result in results, so read each one: the top-level status is success even when every item failed. Adding items to a change set answers 207 with "status": "partial" when some were rejected.

Most deletes return a short confirmation instead of the whole resource. Deleting an article:

{
  "status": "success",
  "message": "Article moved to trash.",
  "article": {"id": 735, "deleted_at": "2026-09-30T08:23:12+00:00"}
}

Failed requests have "status": "error", "validation_error" or "unauthorized", and some have no status at all: see Errors. Responses may also set cookies. Ignore them: the API authenticates only from headers.

Fields in several languages

Text that a help center can translate is a map from language code to text, never a bare string: an article's title, slug and content, its SEO metadata.title and metadata.description, and a category's name and description. Reads return every language an item has. Writes change only the languages you send, and leave the others as they are.

curl https://api.helpcenter.io/v1/articles \
  -H "apikey: $HELPCENTER_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
    "title": { "en": "Cancel your subscription", "de": "Abonnement kündigen" },
    "content": {
      "en": "<p>Cancel at any time under <strong>Account</strong> &rarr; <strong>Billing</strong>.</p>",
      "de": "<p>Sie können jederzeit unter <strong>Konto</strong> &rarr; <strong>Abrechnung</strong> kündigen.</p>"
    },
    "category_id": 316
  }'

The response, trimmed to the fields in several languages:

{
  "status": "success",
  "action": "created",
  "article": {
    "id": 736,
    "title": {"en": "Cancel your subscription", "de": "Abonnement kündigen"},
    "slug": {"en": "cancel-your-subscription", "de": "abonnement-kundigen"},
    "content": {
      "en": "<p>Cancel at any time under <strong>Account</strong> &rarr; <strong>Billing</strong>.</p>",
      "de": "<p>Sie können jederzeit unter <strong>Konto</strong> &rarr; <strong>Abrechnung</strong> kündigen.</p>"
    },
    "published": false,
    "published_translations": []
  }
}

Language codes. A key is one of these codes, spelled exactly: ar, bg, ca, cs, da, de, de-AT, de-CH, el, en, es, eu, fa, fi, fil, fr, ga, gl, he, hi, hu, hy, id, it, ja, ko, lt, lv, ms, mt, mul (mixed languages), nl, no, pl, pt, ro, ru, sv, th, tr, uk, vi, zh-Hans, zh-Hant. Portuguese is pt; there is no pt-BR.

Enabled languages only. A key must also be your help center's default language or one you added to it. Otherwise article writes answer 400:

{
  "status": "error",
  "message": "Unsupported language key \"fr\" provided."
}

Category writes answer 422 with a validation_error on the field instead, such as "fr" is not enabled on this help center, so it cannot key a name.

Unreleased translations. Writing a second language doesn't show it to readers. A translation stays unreleased until someone releases it in the dashboard or publishes a staged edit that releases it, so published_translations above is still empty. See Release translations and change your default language and Staged changes and versions.

lang and locale

Several endpoints take a lang or locale parameter, and they don't all do the same thing:

Parameter

Endpoints

What it does

lang

GET /v1/articles

Picks the language that search matches in. It doesn't filter the response: every article still comes back with all its languages.

lang

GET /v1/comments, GET /v1/articles/{id}/comments, GET /v1/feedback, /v1/analytics/*

Keeps one language. It must be one of your help center's languages, or the request fails with 422.

lang

GET /v1/articles/{id}/notes

Keeps one language. It isn't checked, so a typo returns an empty list.

lang

POST /v1/articles/{id}/comments, POST /v1/articles/{id}/feedback

The language of what you post. Feedback in a language your help center doesn't have is recorded in its default language.

lang

GET /v1/translations

Narrows each interface text to one language.

locale

/v1/articles/{id}/staged, /v1/articles/{id}/versions

The one language a staged change or a version belongs to.

Dates and times

Every time is in UTC. The API uses two formats:

Format

Example

Where

YYYY-MM-DD HH:MM:SS, no offset

2026-09-30 08:23:12

created_at and updated_at of articles, categories, images and sites

ISO 8601 with an offset

2026-09-30T08:23:12+00:00

The other timestamps, such as deleted_at and the times in staged changes, versions, change sets, comments, team notes, reader feedback, webhooks and the export

Read the first format as UTC. Analytics date ranges (from and to) are plain dates, YYYY-MM-DD. When you send a time, send it in UTC too: GET /v1/articles?updated_since= ignores an offset, so 2026-09-30T10:00:00+02:00 is read as 10:00 UTC, two hours later than you meant. GET /v1/feedback?updated_since= reads the offset.

IDs

IDs are integers: articles, categories, images, comments, webhooks, change sets and help centers. A help center also has a uuid. An article can also carry your own key, external_id, of up to 191 characters: send it with POST /v1/articles, and a later request with the same external_id updates that article instead of creating another. External ids are matched exactly, upper and lower case included.

Article content: HTML and Markdown

Article content is HTML. HelpCenter.io stores the HTML you send as it is, apart from removing spaces at the start and end, <style> elements, and markup that would run script on your help center's pages, such as <script> elements, event attributes like onclick and javascript: addresses. To write Markdown instead, add "content_format": "markdown" to POST /v1/articles, to PATCH /v1/articles/{id} or to a bulk item. The API converts every language of content to HTML with GitHub Flavored Markdown: tables, fenced code blocks and task lists work, raw HTML passes through, and javascript: links are dropped.

curl https://api.helpcenter.io/v1/articles \
  -H "apikey: $HELPCENTER_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
    "title": { "en": "Export your data" },
    "content_format": "markdown",
    "content": {
      "en": "Open **Settings**, then click **Export**.\n\n1. Choose a format.\n2. Click **Download**.\n\n```bash\ncurl -H \"apikey: $HELPCENTER_API_KEY\" https://api.helpcenter.io/v1/export\n```"
    },
    "category_id": 293
  }'

The article stores and returns HTML. The response, trimmed to the content:

{
  "status": "success",
  "action": "created",
  "article": {
    "id": 737,
    "title": {"en": "Export your data"},
    "content": {
      "en": "<p>Open <strong>Settings</strong>, then click <strong>Export</strong>.</p>\n<ol>\n<li>Choose a format.</li>\n<li>Click <strong>Download</strong>.</li>\n</ol>\n<pre><code class=\"language-bash\">curl -H &quot;apikey: $HELPCENTER_API_KEY&quot; https://api.helpcenter.io/v1/export\n</code></pre>\n"
    }
  }
}

Reads always return HTML, never Markdown. They also leave out what only your team sees: internal notes, AI drafts waiting for review, and the highlights of team notes (the highlighted text stays). If you read an article, change its HTML and write it back, the internal notes and AI drafts are gone from the article; the highlights of team notes that are still open stay where their text is still there. Change only what you mean to, and send only the languages you edited.

Booleans

In a JSON body, send true and false. In a query string or a form field, send 1 and 0: the words true and false fail validation there.

curl "https://api.helpcenter.io/v1/comments?include_email=true" \
  -H "apikey: $HELPCENTER_API_KEY" \
  -H "Accept: application/json"

The response:

{
  "status": "validation_error",
  "message": "The request could not be accepted.",
  "errors": {
    "include_email": ["The include email field must be true or false."]
  }
}

With include_email=1, the same request succeeds. Two parameters are lenient: force on DELETE /v1/images/{id} also accepts true, and with_message on GET /v1/feedback accepts true and false.

Strings and empty values

The API removes spaces at the start and end of every string you send, HTML and Markdown included, and turns an empty string into null, except on the design endpoints under /v1/template, which take strings as you send them (see Design API). So an empty string in a language map clears that language: "metadata": {"description": {"en": ""}} removes the English SEO description, and "slug": {"en": ""} makes the English slug again from the title. A category's name can't be cleared this way: an empty one fails validation. Outside language maps, an empty string doesn't clear anything: "external_id": "" is ignored, and "visibility": "" fails validation. To keep a field as it is, leave it out of the request.

Send a title with text in every language you include. An empty or blank title is refused, naming the language: POST /v1/articles answers 400 and PATCH answers 422, and nothing is changed.

Retrying safely

  • POST /v1/articles and POST /v1/articles/bulk with an external_id are safe to retry: the second request updates the article the first one created.

  • Image uploads are matched by content: by default, uploading the same file again returns the image you already have, with "reused": true.

  • Other creates aren't: retrying POST /v1/articles/{id}/comments posts a second comment. The API has no Idempotency-Key header, and responses carry no request id.

Calling the API from a browser

The API answers cross-origin requests from any website (Access-Control-Allow-Origin: *). Even so, never put an API key in code that runs in a browser, where anyone can read it: call the API from your server. The widget and single sign-on sign readers in with JWTs that your server creates, not with API keys. Browser code also can't read the rate-limit headers, because the API doesn't expose them to it.

Size limits

Limit

Value

Request body

32 MB. A larger request gets 413 Payload Too Large.

One image

10 MB

Images in one POST /v1/images/bulk

20

Articles in one POST /v1/articles/bulk

50

How many requests you can make is covered in Rate limits, and how lists are split into pages in Pagination.

Was this article helpful?