# API keys

_Category: Authentication_

An API key lets your own code call the HelpCenter.io REST API for one help center. Send it in the `apikey` header of every request to `https://api.helpcenter.io/v1`. It keeps working until you delete it.

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

The response names the help center the key belongs to, and only that one:

```
{
  "status": "success",
  "sites": [
    {
      "id": 1234,
      "uuid": "8f14e45f-ceea-4f6a-9d3b-2c5e1a7b9c01",
      "name": "Acme Help",
      "subdomain": "acme",
      "domain": "acme.helpcenter.io",
      "url": "https://acme.helpcenter.io",
      "default_language": "en",
      "visibility": "public",
      "publicly_accessible": true,
      "languages": ["en"],
      "created_at": "2026-09-30 08:03:11"
    }
  ],
  "meta": {
    "items_count": 1
  }
}
```

## Create a key

**Who can do this:** Owners and Admins of the help center · **Plans:** All plans

In the dashboard, click the gear icon, then **Settings**. Under **API keys**, click **New key**, give the key a **Name** that says where it will be used, pick a **Scope**, and click **Create key**. Leave **Team notes** off unless your integration works with your team's notes, and **Design** off unless it reads or changes your help center's design. The steps are in [Create an API key](https://self.helpcenter.io/content/create-an-api-key).

![Screen recording. Under API keys, click New key. Name it after where it will be used, and pick a Scope. Click Create key, then Copy it somewhere safe.](https://helpcenter-io.s3.amazonaws.com/uploads/self/Ixbsnu8aauTTHSm1dLEwsos9dgUrKZI9INdmuhXH.gif)
Create the key in Settings, then copy it.

A key is a 64-character string. Click **Copy** and store it where your code reads its secrets, such as an environment variable or a secret manager. The samples on this site read it from `HELPCENTER_API_KEY`.

## What a key can do

The scope you pick decides which endpoints the key can call. A key holds these [scopes](https://developers.helpcenter.io/content/scopes):

| Option | Scopes | What the key can do |
| --- | --- | --- |
| **Read only** | `content.read`, `analytics.read` | Read articles, categories, images, comments, staged changes and versions, change sets, interface translations and exports, and read analytics. |
| **Read & write** | `content.read`, `content.write`, `analytics.read`, `webhooks.manage` | Everything a Read only key can do, plus create, update and delete content, upload images, moderate comments, publish staged changes and change sets, and manage webhooks. |
| **Team notes** (optional, off by default) | `notes.read`, plus `notes.write` on a **Read & write** key | Read the private notes your team leaves on articles. With **Read & write**, also reply to them and resolve them. |
| **Design** (optional, off by default) | `design.read`, plus `design.write` on a **Read & write** key | Tick **Allow access to the help center design** to let the key read the help center's design. With **Read & write**, it can also change the design and publish it, custom CSS and scripts included. See [Design API](https://developers.helpcenter.io/content/design-api). |

A request outside the key's scopes fails with `403 Forbidden`, and the message names the scope the key lacks:

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

You choose the scope, **Team notes** access and **Design** access when you create a key, and you can't change them later: an existing key never gains design access. Team notes requests also need the person who created the key to have access to team notes in the help center, as an Owner, Admin, Editor or Translator; without it, they fail with `403` and the code `notes_forbidden`. Design requests need that person to be an Owner, Admin or Editor; otherwise they fail with `403` and the code `design_forbidden`.

## One key, one help center

A key always acts on the help center it was created in. OAuth apps pick a help center with the `X-HCio-Site` header or the `site` query parameter; with an API key, both are ignored. To work with several help centers, create a key in each one, or use [OAuth](https://developers.helpcenter.io/content/oauth).

## Send the key

- Send the key in the `apikey` header. The header name is not case-sensitive. The API reads the key from no other header or query parameter.
- Also send `Accept: application/json`, so that errors come back as JSON (see [Requests and responses](https://developers.helpcenter.io/content/requests-responses-and-conventions)).
- If a request carries both an `apikey` header and an `Authorization: Bearer` token, only the key is read. A valid token doesn't rescue an invalid key: the request fails.
- Each key has its own rate limit budget. See [Rate limits](https://developers.helpcenter.io/content/rate-limits).

A missing, mistyped or deleted key gets `401 Unauthorized` with this body. So does a key whose creator's HelpCenter.io account was deleted.

```
{
  "status": "unauthorized"
}
```

## Check a key

This program reports which help center a key reaches and whether it can write. `GET /v1/account` returns `kb.capabilities`, with `read` and `write` set for the credential that calls it.

```
// Check which help center an API key reaches and whether it can write.
// Node.js 18 or later, no dependencies. Run: node check-key.js
const API = 'https://api.helpcenter.io/v1';

async function get(path) {
  const res = await fetch(API + path, {
    headers: { apikey: process.env.HELPCENTER_API_KEY, Accept: 'application/json' },
  });
  const text = await res.text();
  if (!res.ok) throw new Error(`GET ${path} failed with ${res.status}: ${text}`);
  return JSON.parse(text);
}

async function main() {
  const { sites } = await get('/sites');
  const { kb } = await get('/account');
  console.log(`This key works with help center ${sites[0].id} (${sites[0].url}).`);
  console.log(`Read: ${kb.capabilities.read ? 'yes' : 'no'}. Write: ${kb.capabilities.write ? 'yes' : 'no'}.`);
}

main().catch((err) => {
  console.error(err.message);
  process.exit(1);
});
```

```
# Check which help center an API key reaches and whether it can write.
# Python 3.8 or later, standard library only. Run: python3 check_key.py
import json
import os
import sys
import urllib.error
import urllib.request

API = "https://api.helpcenter.io/v1"

def get(path):
    request = urllib.request.Request(
        API + path,
        headers={"apikey": os.environ["HELPCENTER_API_KEY"], "Accept": "application/json"},
    )
    try:
        with urllib.request.urlopen(request) as response:
            return json.load(response)
    except urllib.error.HTTPError as err:
        sys.exit(f"GET {path} failed with {err.code}: {err.read().decode()}")

sites = get("/sites")["sites"]
capabilities = get("/account")["kb"]["capabilities"]
print(f"This key works with help center {sites[0]['id']} ({sites[0]['url']}).")
print(
    f"Read: {'yes' if capabilities['read'] else 'no'}. "
    f"Write: {'yes' if capabilities['write'] else 'no'}."
)
```

```
<?php
// Check which help center an API key reaches and whether it can write.
// PHP 8 or later with the curl extension. Run: php check-key.php

const API = 'https://api.helpcenter.io/v1';

function get(string $path): array
{
    $ch = curl_init(API . $path);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_HTTPHEADER, [
        'apikey: ' . getenv('HELPCENTER_API_KEY'),
        'Accept: application/json',
    ]);
    $raw = curl_exec($ch);
    $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
    if ($raw === false || $status >= 400) {
        fwrite(STDERR, "GET $path failed with $status: $raw\n");
        exit(1);
    }

    return json_decode($raw, true);
}

$sites = get('/sites')['sites'];
$capabilities = get('/account')['kb']['capabilities'];
echo "This key works with help center {$sites[0]['id']} ({$sites[0]['url']}).\n";
echo 'Read: ' . ($capabilities['read'] ? 'yes' : 'no')
    . '. Write: ' . ($capabilities['write'] ? 'yes' : 'no') . ".\n";
```

With a **Read & write** key, it prints:

```
This key works with help center 1234 (https://acme.helpcenter.io).
Read: yes. Write: yes.
```

## Manage your keys

- **You see only your own keys.** Settings lists the keys you created, masked, each with its scope, a **Team notes** tag if it has notes access, and a **Design** tag if it has design access. You see and delete only your keys; other Owners and Admins see only theirs.
- **You can copy a key again.** **Copy** copies the full key whenever you need it.
- **Deleting is immediate.** Click the trash icon next to the key, then **Delete key**. The next request with that key gets `401`.
- **Keys don't expire, and there is no rotate button.** To replace a key, create a new one, switch your integration to it, then delete the old one.
- **When someone leaves your team.** Before you remove someone, ask them to delete the keys they created, and replace any integration that used them.
- **Keys go with their help center.** When a help center is deleted, its keys stop working.

## Keep keys secret

- Use keys on servers, in scheduled jobs and in CI. Never put a key in browser JavaScript, a mobile app or a code repository: anyone who can load the page, install the app or read the repository can read the key.
- The widget never needs an API key, and neither does single sign-on: signing readers in uses a JWT (see [Sign readers into the widget with a JWT](https://developers.helpcenter.io/content/widget-jwt) and [How JWT single sign-on works](https://developers.helpcenter.io/content/how-jwt-sso-works)).
- Create one key per integration, so that you can delete one without breaking the others, and pick **Read only** unless the integration writes.
- If a key leaks, create a replacement, switch to it, and delete the leaked key.
- A **Read & write** key with **Design** access can change the custom CSS and scripts that every visitor's browser runs once the design is published. Treat it like an admin password, and delete it when the work is done.

## What a key can't do

- **Reach another help center.** Each key works with the help center it was created in, and nothing else.
- **Act for other people.** If you're building an app that other HelpCenter.io customers connect to their help centers, use [OAuth](https://developers.helpcenter.io/content/oauth): each person approves your app and picks their help centers.
- **Narrow itself further.** A key has no expiry date, no IP address restriction and no per-endpoint permissions beyond its scope.
- **Use the MCP server on every plan.** Through the [HelpCenter.io MCP server](https://developers.helpcenter.io/content/the-mcp-connector-for-ai-agents) for AI agents, a key works only for a help center on the Growth or Catalyst plan. For a help center on another plan, every tool call is refused with a message that the MCP server is available on the Growth and Catalyst plans. Called directly, the REST API works with a key on every plan.

## API key or OAuth

|  | API key | OAuth access token |
| --- | --- | --- |
| Made for | Your own scripts and servers | Apps that act for a HelpCenter.io user, including AI assistants |
| Help centers | The one it was created in | The ones the person shared with your app |
| Header | `apikey: <key>` | `Authorization: Bearer <token>` |
| Permissions | **Read only** or **Read & write**, plus **Team notes** and **Design** | The scopes your app requested and the person approved |
| Lifetime | Until you delete it | 1 hour. The refresh token lasts 30 days, and each refresh returns a new one. |
| Change sets | Yes | No: API keys only |
| Revoked by | Deleting the key | The person, in **Connected Apps** |

## Related

- [Scopes](https://developers.helpcenter.io/content/scopes)
- [OAuth 2.1 for apps and AI clients](https://developers.helpcenter.io/content/oauth)
- [Quickstart: your first API request](https://developers.helpcenter.io/content/quickstart-your-first-request)
- [Rate limits](https://developers.helpcenter.io/content/rate-limits)
