# API authentication, tokens and scopes

The API authenticates every call with a **bearer token** you create in the app. A token carries the account it
belongs to, the **scopes** (operations) it may perform, an expiry and, optionally, an IP allow-list. The same token
works for raw HTTP calls, the [SDKs](/integrations/sdks/), [ht-cli](/integrations/cli/), the
[Terraform provider](/integrations/terraform/) and the [MCP server](/integrations/mcp/).

## Before you start

Your plan must include API access - the app refuses to create a token otherwise. See
[which plans include the API](/integrations/rate-limits/#which-plans-include-the-api). Subaccounts need the API
access right.

## Token settings

| Setting (UI label) | Mint field | Allowed values | Default | What it does for you |
|---|---|---|---|---|
| **Token name** | `name` | Free text (placeholder "e.g. CI server") | Empty | Lets you recognise the token later. |
| **Token scopes (allowed operations)** | `scopes` | Any of the scopes below; at least one | None ticked | What the token may do. A token with no scope is refused. |
| **Token expiration** | `tokenDuration` or `tokenExpiration` | **10 years**, **1 day**, **7 days**, **30 days**, **120 days**, **1 year**, **Custom date** | 10 years | When the token stops working. |
| **Token IP white list** (optional) | `ipWhiteList` | Up to 10 entries: an address (`1.2.3.4`) or a range (`1.2.3.4-4.3.2.1`) | Empty (any address) | Calls from any other address answer `403 ip_not_allowed`. |

## Create a token

1. Open **Integrations -> API** in the app (`/integrations/api`).
2. In **New access token**, enter a **Token name**.
3. Under **Token scopes (allowed operations)**, tick the scopes the integration needs (each family has **All** and
   **None** shortcuts).
4. Choose a **Token expiration**. Prefer a short one for anything you hand to a person, a CI job or an AI tool.
5. Optionally list the addresses allowed to use it in **Token IP white list**.
6. Click **Create Access Token** and copy the token. It is shown **once** and is not stored on HostTracker's side -
   if you lose it, create another.

## Use the token

Send it on every request:

```
Authorization: Bearer YOUR_HOSTTRACKER_API_TOKEN
```

```bash
curl https://api2.host-tracker.com/account -H "Authorization: Bearer $HT_TOKEN"
```

There is no account id in URLs or bodies - the token says who is calling.

## Scopes

There are 13 leaf scopes, one per action on an area, and 7 family scopes (the bare area name):

| Family | Leaves | Covers |
|---|---|---|
| `monitor` | `monitor:read`, `monitor:write` | Monitors, results, incidents, uptime summaries, maintenance windows, reports, per-monitor subscriptions |
| `contact` | `contact:read`, `contact:write` | Contacts, contact groups, confirmations, tests, the notification log, per-contact subscriptions |
| `subs` | `subs:read` | The flat account-wide subscription lists (`GET /alert`, `GET /report`) |
| `webhook` | `webhook:read`, `webhook:write` | Webhooks, their delivery log, tests and redelivery |
| `statuspage` | `statuspage:read`, `statuspage:write` | Status pages, components, declared incidents, templates, subscribers |
| `account` | `account:read`, `account:write` | Account details, usage, limits, quota; `account:write` changes profile, time zone, language and default locations |
| `check` | `check:read`, `check:write` | Instant checks |

Rules:

- **A leaf does not imply its sibling.** `monitor:write` does not grant `monitor:read`, and the reverse is also
  false. An integration that both reads and writes monitors needs both leaves, or the family.
- **A family covers every leaf in it** - `monitor` passes both `monitor:read` and `monitor:write`. A family also
  grows automatically if its area gains a new action later; grant leaves when you want the token pinned to today's
  surface.
- **A write that reaches two areas needs both.** Creating a monitor with inline `contacts` needs `monitor:write`
  and `contact:write`; the bulk subscription writes (`POST /alert/bulk`, `POST /report/bulk`) need both writes too.
- **Subscription writes ride their parent**: `PUT /monitor/{id}/alert/{contactId}` needs `monitor:write`, the
  mirror `PUT /contact/{id}/alert/{monitorId}` needs `contact:write`.
- **Jobs** need the scope of the operation that created them.
- The old spellings `ic`, `ic:read`, `ic:write` and `subs:write` no longer exist.

`GET /account/quota` returns the full scope catalogue with a one-line description of each, and shows which scopes
the calling token carries.

### Common scope sets

| Integration | Scopes |
|---|---|
| Status dashboard (read-only) | `monitor:read` |
| Provisioning script (create monitors, never read them back) | `monitor:write` |
| Monitors as code (Terraform, CI) | `monitor` (plus `contact`, `webhook`, `statuspage` for those resources; `account:read` for the account data source) |
| Deploy smoke test with instant checks | `check` |
| Alert routing sync | `contact`, `monitor:write`, `subs:read` |
| Status page automation | `statuspage` |

## What a refusal looks like

| Status and code | Meaning | Fix |
|---|---|---|
| `401 invalid_token`, reason `missing` | No `Authorization` header. | Send the header. |
| `401 invalid_token`, reason `invalid` | Malformed, tampered or expired token (the two look identical on purpose). | Mint a new token. |
| `403 missing_scope` | The token is valid but lacks the scope. `errors[0]` carries `required` and `granted`. | Mint a token with the missing scope - scopes cannot be added to an existing token. |
| `403 ip_not_allowed` | Called from an address outside the token's allow-list. `errors[0].clientIp` shows your address. | Call from an allowed address or mint a new token. |
| `403 insufficient_rights` | A [subaccount](/account/subaccounts/) token lacks the subaccount right for this operation. | Ask the account owner to grant the right. |

```json
{
  "code": "missing_scope",
  "status": 403,
  "errors": [ { "required": "monitor:write", "granted": ["monitor:read"] } ]
}
```

## Token security

A token is a password for its scopes. It is **not stored** on HostTracker's side - the grant lives inside the
signed token - so:

- **A token cannot be revoked before it expires.** Changing your password or signing out everywhere does not stop
  it. Only an account-wide API disable (a support action) stops every token at once.
- Limit the damage in advance: scope narrowly, set a short expiration, use the IP allow-list for anything that runs
  from a fixed address, and use one token per integration.
- A token with a past `tokenExpiration` is refused at creation.
- If a token leaks, remove it everywhere, mint a replacement and contact
  [ht2support@host-tracker.com](mailto:ht2support@host-tracker.com). Never send the token itself.

The minting endpoint also accepts a self-cap (the most calls this one token may spend from the account's quota); the
form does not have a field for it yet.

## Connected apps (OAuth)

Some clients - AI assistants such as Claude and ChatGPT connecting to the [MCP server](/integrations/mcp/) - sign
in with **OAuth 2.1** instead of a pasted token. You sign in to HostTracker, see a consent card listing the scopes,
and approve.

- Access tokens last an hour and are refreshed automatically; refresh tokens rotate on every use.
- A client that asks for no scopes gets `check` and `monitor:read`. The `account` family is never granted to a
  connected app.
- Unlike pasted tokens, a connected app **can be revoked**: **Integrations -> API -> Connected apps** lists every
  app with its scopes and connection date; **Revoke** cuts it off at once (its last access token expires within the
  hour).

## Related

- [REST API v2](/integrations/rest-api/)
- [Rate limits and quotas](/integrations/rate-limits/)
- [Connect an AI assistant with MCP](/integrations/mcp/)
- [Subaccounts](/account/subaccounts/)
