# Sites and account

_Category: REST API reference_

Three endpoints tell your integration what it is connected to: `GET /v1` checks that a credential works, `GET /v1/sites` lists the help centers it can act on, and `GET /v1/account` returns the account, the plan and what the credential may do. Call them when you connect a new API key or OAuth token, before you touch any content.

## Endpoints

| Method and path | What it does | Scope |
| --- | --- | --- |
| `GET /v1` | Checks that a credential is valid | Any valid credential |
| `GET /v1/sites` | Lists the help centers the credential can act on | `content.read` |
| `GET /v1/account` | Returns the account, plan and capabilities behind the credential | Any valid credential |

Every API key holds `content.read`, so all three work with any key. An OAuth token without `content.read` can call `GET /v1` and `GET /v1/account`, and gets `403 Forbidden` with `"code": "insufficient_scope"` from `GET /v1/sites`. See [Scopes](https://developers.helpcenter.io/content/scopes).

## Check a credential

GET`/v1`

Check that an API key or OAuth token is valid.

A `200 OK` means the key or token is valid. The banner needs no scope, so a Read only key and a Read & write key get the same answer. To find out what a credential may do, call `GET /v1/account` (below) and read `kb.capabilities`.

Every `/v1` endpoint that takes a key or token answers `401 Unauthorized` with `{"status": "unauthorized"}` when the `apikey` header is missing, when the key is mistyped or deleted, when the person who created the key no longer has a HelpCenter.io account, and when an OAuth token is expired, revoked or malformed. The body never says which of these it was.

The API's root, `https://api.helpcenter.io/`, answers the same message with no credential at all. A `200` from it proves nothing about your key, so always check `/v1`.

## The site object

A help center, as `GET /v1/sites` returns it:

| Field | Type | Description |
| --- | --- | --- |
| `id` | integer | The help center's id. With an OAuth token, send it in the `X-HCio-Site` header to choose the help center a request acts on. |
| `uuid` | string | A stable public identifier. Export pages and webhook deliveries carry the same value as `helpcenter_id`. |
| `name` | string | The help center's name. |
| `subdomain` | string | The name in its `<subdomain>.helpcenter.io` address. |
| `domain` | string | Where readers find it: the custom domain when one is live, otherwise `<subdomain>.helpcenter.io`. |
| `url` | string | `https://` followed by `domain`. |
| `default_language` | string | The default language code, for example `en`. |
| `visibility` | string | `public`, `private` or `password`, as set under [Control who can see your help center](https://self.helpcenter.io/content/who-can-see-your-help-center). |
| `publicly_accessible` | boolean | `true` when anyone can open the help center: it is public, it is not suspended, and it has no IP allow-list. |
| `languages` | array of strings | The default language first, then every additional language. These are the keys the API accepts in per-language maps such as an article's `title`. |
| `created_at` | string | When the help center was created, in UTC, as `YYYY-MM-DD HH:MM:SS`. |

## List help centers

GET`/v1/sites`

List the help centers this credential can act on.

What you get depends on the credential:

- **An API key** belongs to one help center, so the list holds exactly that one. The `X-HCio-Site` header and the `site` query parameter are ignored for API keys.
- **An OAuth token** sees only the help centers that meet both conditions: the HelpCenter.io account of the person who approved your app owns them, and the person shared them with your app, on the consent screen or later in **Connected Apps**. The API checks both on every request, so a change shows in the list on your next request. Use each `id` in `X-HCio-Site`, as described in [Choose which help center a request acts on](https://developers.helpcenter.io/content/choosing-a-help-center).

The list is ordered by `id` and has no pages: `meta.items_count` is its length.

## Get the account

GET`/v1/account`

Get the account, plan and capabilities behind this credential.

The response has three blocks:

| Field | Type | Description |
| --- | --- | --- |
| `account` | object | The HelpCenter.io account the credential acts for: `id`, `email` and `name` of the account's owner. For an API key, this is the account of the person who created the key. If they were invited to the help center from another HelpCenter.io account, you see their own account, not the one that owns the help center. |
| `provider` | string | Always `helpcenter.io`. |
| `plan` | object | The account's plan. See the next table. |
| `kb.sites_count` | integer | How many help centers this credential can act on. |
| `kb.site_ids` | array of integers | Their ids, the same ones `GET /v1/sites` lists. |
| `kb.capabilities.read` | boolean | `true` when the credential holds `content.read`. |
| `kb.capabilities.write` | boolean | `true` when it holds `content.write`. A Read only key reports `false`. |

The `plan` block:

| Field | Type | Description |
| --- | --- | --- |
| `name` | string | The plan's identifier, for example `catalyst_annual`. |
| `title` | string | The plan's display title, or its `name` when it has none. |
| `active` | boolean | `true` when the account pays for its plan. An account in its free trial without a subscription reports `false`, with `on_trial: true`. |
| `on_trial` | boolean | `true` during the free trial. |
| `ai_included` | boolean | `true` when the plan includes the AI features. |
| `specs` | object | Feature flags of the plan, such as `sso` and `staged_changes`. A plan without flags returns `[]`. |
| `source` | string | Where the active plan comes from: `subscription`, `chat_bundle`, `both` or `none`. |
| `via_bundle`, `bundle` | boolean, object or null | `via_bundle` is `true` when a bundle provides the plan, alone or with a subscription, and `bundle` then holds its details or `null`. Otherwise `false` and `null`. |

Read `kb.capabilities.write` before your integration offers to create or change content, so a Read only key gets a clear message instead of a `403` halfway through a job.

This script prints the help centers a key reaches and whether it can write:

```
// Check which help centers a key reaches and whether it can write.
const API = 'https://api.helpcenter.io/v1';
const headers = { apikey: process.env.HELPCENTER_API_KEY, Accept: 'application/json' };

async function get(path) {
  const res = await fetch(API + path, { headers });
  if (!res.ok) throw new Error(`${path}: HTTP ${res.status}`);
  return res.json();
}

async function main() {
  const account = await get('/account');
  const { sites } = await get('/sites');
  for (const site of sites) {
    console.log(`${site.name} (${site.url}), languages: ${site.languages.join(', ')}`);
  }
  const canWrite = account.kb.capabilities.write;
  console.log(canWrite
    ? 'This key can create and change content.'
    : 'This key can only read.');
}

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

```
# Check which help centers a key reaches and whether it can write.
import json
import os
import urllib.request

API = "https://api.helpcenter.io/v1"
HEADERS = {"apikey": os.environ["HELPCENTER_API_KEY"], "Accept": "application/json"}

def get(path):
    request = urllib.request.Request(API + path, headers=HEADERS)
    with urllib.request.urlopen(request) as response:
        return json.load(response)

account = get("/account")
for site in get("/sites")["sites"]:
    print(f"{site['name']} ({site['url']}), languages: {', '.join(site['languages'])}")
if account["kb"]["capabilities"]["write"]:
    print("This key can create and change content.")
else:
    print("This key can only read.")
```

```
<?php
// Check which help centers a key reaches and whether it can write.
$api = 'https://api.helpcenter.io/v1';
$headers = ['apikey: ' . getenv('HELPCENTER_API_KEY'), 'Accept: application/json'];

function get(string $url, array $headers): array
{
    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER => $headers,
    ]);
    $body = curl_exec($ch);
    $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
    if ($status !== 200) {
        throw new RuntimeException("$url: HTTP $status");
    }
    return json_decode($body, true);
}

$account = get("$api/account", $headers);
foreach (get("$api/sites", $headers)['sites'] as $site) {
    $languages = implode(', ', $site['languages']);
    echo "{$site['name']} ({$site['url']}), languages: $languages\n";
}
echo $account['kb']['capabilities']['write']
    ? "This key can create and change content.\n"
    : "This key can only read.\n";
```

For the Acme help center in the examples on this page and a Read & write key, it prints `Acme (https://acme.helpcenter.io), languages: en, de` and `This key can create and change content.`

## Related

- [Choose which help center a request acts on](https://developers.helpcenter.io/content/choosing-a-help-center)
- [API keys](https://developers.helpcenter.io/content/api-keys)
- [OAuth 2.1 for apps and AI clients](https://developers.helpcenter.io/content/oauth)
- [Articles](https://developers.helpcenter.io/content/articles-api)
