# Requests and responses

_Category: Getting started_

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` with `{"status":"unauthorized"}`.

## 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](https://developers.helpcenter.io/content/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](https://developers.helpcenter.io/content/oauth). 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](https://developers.helpcenter.io/content/choosing-a-help-center). |
| `Accept: application/json` | Always | Makes errors JSON wherever the API can, and stops validation errors from turning into redirects. 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.

## Always send Accept: application/json

Without `Accept: application/json`, two things change:

- General errors, such as `405 Method Not Allowed`, `429 Too Many Requests` from most limits, and `500 Internal Server Error`, come back as HTML pages instead of JSON.
- `GET /v1/categories`, `GET /v1/images` and `PATCH /v1/articles/{id}` answer invalid input with a `302` redirect to `https://api.helpcenter.io` instead of an error. A client that follows redirects then receives the API's welcome message with `200 OK`, and can mistake the failed request for a success.

For example, `"published": "yes"` isn't a valid value. Sent without the header, the update is refused with a redirect:

```
HTTP/1.1 302 Found
Location: https://api.helpcenter.io
```

With `Accept: application/json`, the same request gets the actual error, `422 Unprocessable Entity`:

```
{
  "message": "The published field must be true or false.",
  "errors": {
    "published": ["The published field must be true or false."]
  }
}
```

A few `404 Not Found` answers are HTML pages even with the header: an unknown path, and an unknown id on `PATCH` or `DELETE /v1/articles/{id}`, `PATCH` or `DELETE /v1/categories/{id}` and `PUT /v1/categories/{id}/articles/order`. Check the status code before you parse a body. [Errors](https://developers.helpcenter.io/content/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](https://developers.helpcenter.io/content/images-api). 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](https://developers.helpcenter.io/content/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](https://self.helpcenter.io/content/manage-languages) and [Staged changes and versions](https://developers.helpcenter.io/content/staged-changes-api).

### 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`, `/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, 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.

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

## 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. 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, those notes, drafts and highlights are gone from the article. 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. One parameter is lenient: `force` on `DELETE /v1/images/{id}` also accepts `true`.

## 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](https://developers.helpcenter.io/content/design-api)). So an empty string in a language map clears that language: `"metadata": {"description": {"en": ""}}` removes the English SEO description, and `"slug": {"en": ""}` leaves the article without an English slug. 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. `POST /v1/articles` with an empty or blank title fails with `500 Internal Server Error`.

## 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](https://developers.helpcenter.io/content/rate-limits), and how lists are split into pages in [Pagination](https://developers.helpcenter.io/content/pagination).

## Related

- [Pagination](https://developers.helpcenter.io/content/pagination)
- [Errors](https://developers.helpcenter.io/content/errors)
- [Rate limits](https://developers.helpcenter.io/content/rate-limits)
- [Articles](https://developers.helpcenter.io/content/articles-api)
