# REST API

The REST API lets you create and manage VPN monitors and their groups, run tests, read results and incidents, manage subscription URLs, run your [status pages](https://site.dev.tunnelhq.com/docs/concepts/status-pages/), plan [maintenance](https://site.dev.tunnelhq.com/docs/concepts/maintenance/), manage [servers](https://site.dev.tunnelhq.com/docs/concepts/servers/), and choose which [alert channels](https://site.dev.tunnelhq.com/docs/alert-channels/) a monitor's alerts go to. The channels themselves, outbound webhooks, team members, and billing are managed in the dashboard.

All endpoints are under `https://app.tunnelhq.com/api/v1`.

The [API reference](https://site.dev.tunnelhq.com/docs/api/reference/) has a page for every endpoint. To generate a client, or to import the API into Postman or Insomnia, <a href="https://site.dev.tunnelhq.com/docs/openapi.yaml" download>download the OpenAPI file</a> (OpenAPI 3.1). It's always at `https://docs.tunnelhq.com/openapi.yaml`.

## Authentication

Send your API key in the `X-API-Key` header, or as `Authorization: Bearer <key>`. Endpoints that act on a project also need an `X-Project-Id` header, which accepts either `42` or `prj_42`. Without it, those endpoints return `400`.

Don't have a key yet? See [Getting an API key](https://site.dev.tunnelhq.com/docs/api-keys/). API access needs the Starter plan or above.

## Example requests

Monitors are exposed as the `/servers` resource, with prefixed IDs such as `srv_916`.

### List monitors

```bash title="List monitors"
curl https://app.tunnelhq.com/api/v1/servers \
  -H "X-API-Key: $TUNNELHQ_API_KEY" \
  -H "X-Project-Id: 42"
```

### Create a monitor

`name` and `protocol` are required. Config-file protocols such as WireGuard take the full config in a `config` field, and the endpoint host and port are read from it.

```bash title="Create a WireGuard monitor"
curl -X POST https://app.tunnelhq.com/api/v1/servers \
  -H "X-API-Key: $TUNNELHQ_API_KEY" \
  -H "X-Project-Id: 42" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Helsinki-I",
    "protocol": "wireguard",
    "config": "[Interface]\nPrivateKey = ...\nAddress = 10.0.0.2/32\n\n[Peer]\nPublicKey = ...\nEndpoint = vpn.example.com:51820\nAllowedIPs = 0.0.0.0/0"
  }'
```

You can also set `timeout`, `maxretries`, and `interval` in seconds. The interval can be anything from your plan's fastest interval up to `86400` (24 hours); a shorter one returns `403`. If you leave it out, it's 3 minutes or your plan's fastest interval, whichever is longer.

### Run an on-demand test

`POST /test` runs a check against an existing monitor and waits up to 75 seconds for the result.

```bash title="Run an on-demand test"
curl -X POST https://app.tunnelhq.com/api/v1/test \
  -H "X-API-Key: $TUNNELHQ_API_KEY" \
  -H "X-Project-Id: 42" \
  -H "Content-Type: application/json" \
  -d '{"server_id": "srv_916"}'
```

If the test finishes in time, the response is `200` with the result:

```json title="200: test result"
{
  "test_id": "…",
  "server_id": "srv_916",
  "status": "healthy",
  "latency_ms": 157,
  "connect_ms": 364,
  "error_message": null,
  "tested_at": "…"
}
```

`status` is `healthy`, `down`, or `degraded`. `connect_ms` is how long the tunnel took to come up, and `latency_ms` is one round trip through it, both in milliseconds. If the test is still running after 75 seconds, the response is `202` with `"status": "pending"` and the same `test_id`, `server_id`, and `tested_at`.

To test several monitors at once, send `POST /test/batch` with `{"server_ids": ["srv_916", "srv_917"]}`.

Both endpoints accept two optional fields:

| Field | Description |
| --- | --- |
| `region` | The two-letter region code of the location to test from. Needs the Pro or Business plan; other plans get `402`. |
| `region_fallback` | What happens if the location has no checker of its own and no residential route. `any` (the default) runs the test from the general pool instead, so the result isn't from that location. `fail` doesn't test from anywhere else and reports the location as unavailable. Scheduled checks from a chosen location always use `fail`. |

On-demand tests need a role that can edit monitors. Each monitor tested counts as one test against your plan's monthly on-demand quota.

## Endpoints

Each endpoint links to its page in the [API reference](https://site.dev.tunnelhq.com/docs/api/reference/), with every parameter, response, and error message. Endpoints marked **Project** need the `X-Project-Id` header. **Key only** endpoints need just the API key.

| Method and path | Scope | Description |
| --- | --- | --- |
| [`GET /servers`](https://site.dev.tunnelhq.com/docs/api/reference/operations/list-monitors/) | Project | List monitors. |
| [`POST /servers`](https://site.dev.tunnelhq.com/docs/api/reference/operations/create-monitor/) | Project | Create a monitor. |
| [`GET /servers/:id`](https://site.dev.tunnelhq.com/docs/api/reference/operations/get-monitor/) | Project | Get a monitor. |
| [`PUT /servers/:id`](https://site.dev.tunnelhq.com/docs/api/reference/operations/update-monitor/) | Project | Update a monitor. |
| [`DELETE /servers/:id`](https://site.dev.tunnelhq.com/docs/api/reference/operations/delete-monitor/) | Project | Delete a monitor. |
| [`GET /servers/search?q=`](https://site.dev.tunnelhq.com/docs/api/reference/operations/search-monitors/) | Key only | Search monitors across every project the key can reach. |
| [`GET /groups`](https://site.dev.tunnelhq.com/docs/api/reference/operations/list-groups/) | Project | List groups. |
| [`POST /groups`](https://site.dev.tunnelhq.com/docs/api/reference/operations/create-group/) | Project | Create a group. |
| [`PUT /groups/:id`](https://site.dev.tunnelhq.com/docs/api/reference/operations/rename-group/) | Project | Rename a group. |
| [`DELETE /groups/:id`](https://site.dev.tunnelhq.com/docs/api/reference/operations/delete-group/) | Project | Delete a group. Its monitors stay. |
| [`POST /test`](https://site.dev.tunnelhq.com/docs/api/reference/operations/run-test/) | Project | Run an on-demand test of one monitor. |
| [`POST /test/batch`](https://site.dev.tunnelhq.com/docs/api/reference/operations/run-batch-test/) | Project | Run on-demand tests of several monitors. |
| [`POST /check`](https://site.dev.tunnelhq.com/docs/api/reference/operations/check-config/) | Key only | Check a config without saving a monitor. |
| [`GET /results`](https://site.dev.tunnelhq.com/docs/api/reference/operations/list-results/) | Project | Recent check results. |
| [`GET /results/failures`](https://site.dev.tunnelhq.com/docs/api/reference/operations/list-failures/) | Project | Recent failed checks. |
| [`GET /results/server/:id`](https://site.dev.tunnelhq.com/docs/api/reference/operations/get-monitor-results/) | Project | Check history for one monitor. |
| [`GET /incidents`](https://site.dev.tunnelhq.com/docs/api/reference/operations/list-incidents/) | Project | List incidents. |
| [`GET /incidents/:id`](https://site.dev.tunnelhq.com/docs/api/reference/operations/get-incident/) | Project | Get an incident. |
| [`GET /incidents/:id/events`](https://site.dev.tunnelhq.com/docs/api/reference/operations/list-incident-events/) | Project | An incident's activity log. |
| [`POST /incidents/:id/acknowledge`](https://site.dev.tunnelhq.com/docs/api/reference/operations/acknowledge-incident/) | Project | Acknowledge an incident. |
| [`POST /incidents/:id/retest`](https://site.dev.tunnelhq.com/docs/api/reference/operations/retest-incident/) | Project | Retest the monitor behind an incident. |
| [`GET /subscriptions`](https://site.dev.tunnelhq.com/docs/api/reference/operations/list-subscriptions/) | Project | List subscription URLs. |
| [`POST /subscriptions`](https://site.dev.tunnelhq.com/docs/api/reference/operations/add-subscription/) | Project | Add a subscription URL. |
| [`GET /subscriptions/:id`](https://site.dev.tunnelhq.com/docs/api/reference/operations/get-subscription/) | Project | Get a subscription. |
| [`PUT /subscriptions/:id`](https://site.dev.tunnelhq.com/docs/api/reference/operations/update-subscription/) | Project | Update a subscription. |
| [`DELETE /subscriptions/:id`](https://site.dev.tunnelhq.com/docs/api/reference/operations/delete-subscription/) | Project | Remove a subscription. |
| [`POST /subscriptions/:id/sync`](https://site.dev.tunnelhq.com/docs/api/reference/operations/sync-subscription/) | Project | Sync a subscription now. |
| [`GET /status-pages`](https://site.dev.tunnelhq.com/docs/api/reference/operations/list-status-pages/) | Project | List status pages. |
| [`POST /status-pages`](https://site.dev.tunnelhq.com/docs/api/reference/operations/create-status-page/) | Project | Create a status page with the services you choose. |
| [`GET /status-pages/:id`](https://site.dev.tunnelhq.com/docs/api/reference/operations/get-status-page/) | Project | Get a status page. |
| [`PATCH /status-pages/:id`](https://site.dev.tunnelhq.com/docs/api/reference/operations/update-status-page/) | Project | Update a status page's settings. |
| [`DELETE /status-pages/:id`](https://site.dev.tunnelhq.com/docs/api/reference/operations/delete-status-page/) | Project | Delete a status page. |
| [`PUT /status-pages/:id/services`](https://site.dev.tunnelhq.com/docs/api/reference/operations/set-status-page-services/) | Project | Set the sections and services a page shows. |
| [`PATCH /status-pages/:id/services`](https://site.dev.tunnelhq.com/docs/api/reference/operations/change-status-page-services/) | Project | Add, remove, hide, or show many services at once. |
| [`POST /status-pages/:id/publish`](https://site.dev.tunnelhq.com/docs/api/reference/operations/publish-status-page/) | Project | Publish or unpublish a page. |
| [`PUT /status-pages/:id/domain`](https://site.dev.tunnelhq.com/docs/api/reference/operations/set-status-page-domain/) | Project | Set or remove a page's own domain. |
| [`POST /status-pages/:id/domain/check`](https://site.dev.tunnelhq.com/docs/api/reference/operations/check-status-page-domain/) | Project | Check the domain's record and switch on HTTPS. |
| [`POST /status-pages/:id/brand-from-website`](https://site.dev.tunnelhq.com/docs/api/reference/operations/brand-status-page-from-website/) | Project | Take the logo, favicon, and share image from the page's website. |
| [`GET /status-pages/:id/incidents`](https://site.dev.tunnelhq.com/docs/api/reference/operations/list-status-page-incidents/) | Project | List a page's incidents. |
| [`POST /status-pages/:id/incidents`](https://site.dev.tunnelhq.com/docs/api/reference/operations/create-status-page-incident/) | Project | Post an incident on a page. |
| [`POST /status-pages/:id/incidents/:incidentId/updates`](https://site.dev.tunnelhq.com/docs/api/reference/operations/add-status-page-incident-update/) | Project | Post an update to an incident. |
| [`GET /maintenance`](https://site.dev.tunnelhq.com/docs/api/reference/operations/list-maintenance/) | Project | List maintenance windows. |
| [`POST /maintenance`](https://site.dev.tunnelhq.com/docs/api/reference/operations/create-maintenance/) | Project | Plan a maintenance window. |
| [`GET /maintenance/:id`](https://site.dev.tunnelhq.com/docs/api/reference/operations/get-maintenance/) | Project | Get a maintenance window. |
| [`PATCH /maintenance/:id`](https://site.dev.tunnelhq.com/docs/api/reference/operations/update-maintenance/) | Project | Change, pause, or resume a window. |
| [`POST /maintenance/:id/end`](https://site.dev.tunnelhq.com/docs/api/reference/operations/end-maintenance/) | Project | End a running one-time window now. |
| [`DELETE /maintenance/:id`](https://site.dev.tunnelhq.com/docs/api/reference/operations/delete-maintenance/) | Project | Cancel a window. |
| [`GET /channels`](https://site.dev.tunnelhq.com/docs/api/reference/operations/list-channels/) | Project | List alert channels, without their settings. |
| [`GET /servers/:id/channels`](https://site.dev.tunnelhq.com/docs/api/reference/operations/get-monitor-channels/) | Project | The channels a monitor's alerts go to. |
| [`PUT /servers/:id/channels`](https://site.dev.tunnelhq.com/docs/api/reference/operations/set-monitor-channels/) | Project | Set the channels a monitor's alerts go to. |
| [`POST /servers/:id/test-alert`](https://site.dev.tunnelhq.com/docs/api/reference/operations/send-test-alert/) | Project | Send a test alert through a monitor's channels. |
| [`GET /hosts`](https://site.dev.tunnelhq.com/docs/api/reference/operations/list-hosts/) | Project | List servers that run the agent, with counts by status. |
| [`GET /hosts/incidents`](https://site.dev.tunnelhq.com/docs/api/reference/operations/list-host-incidents/) | Project | Server incidents of the last 30 days, open first. |
| [`GET /hosts/install`](https://site.dev.tunnelhq.com/docs/api/reference/operations/get-host-install-command/) | Project | The command that installs the agent. |
| [`POST /hosts/install/rotate`](https://site.dev.tunnelhq.com/docs/api/reference/operations/rotate-host-install-key/) | Project | Make a new install key. |
| [`GET /hosts/:id`](https://site.dev.tunnelhq.com/docs/api/reference/operations/get-host/) | Project | Get a server in full. |
| [`GET /hosts/:id/history`](https://site.dev.tunnelhq.com/docs/api/reference/operations/get-host-history/) | Project | A server's readings over time. |
| [`PUT /hosts/:id`](https://site.dev.tunnelhq.com/docs/api/reference/operations/update-host/) | Project | Rename a server, or set its group or tags. |
| [`POST /hosts/:id/link`](https://site.dev.tunnelhq.com/docs/api/reference/operations/link-host/) | Project | Link a tunnel or check to a server. |
| [`POST /hosts/:id/unlink`](https://site.dev.tunnelhq.com/docs/api/reference/operations/unlink-host/) | Project | Keep a tunnel or check away from a server. |
| [`PATCH /hosts/:id/alerts`](https://site.dev.tunnelhq.com/docs/api/reference/operations/set-host-alerts/) | Project | Change a server's alerts. |
| [`DELETE /hosts/:id`](https://site.dev.tunnelhq.com/docs/api/reference/operations/remove-host/) | Project | Remove a server. Its agent stays installed. |
| [`POST /hosts/:id/update`](https://site.dev.tunnelhq.com/docs/api/reference/operations/update-host-agent/) | Project | Update a server's agent to the latest release. |
| [`GET /hosts/defaults`](https://site.dev.tunnelhq.com/docs/api/reference/operations/get-host-defaults/) | Project | The project's default for new servers' agent updates. |
| [`PUT /hosts/defaults`](https://site.dev.tunnelhq.com/docs/api/reference/operations/set-host-defaults/) | Project | Change that default. |
| [`GET /whoami`](https://site.dev.tunnelhq.com/docs/api/reference/operations/whoami/) | Key only | Who the key belongs to, the workspaces (with your role) and projects it can reach, and the default workspace and project. |
| [`GET /organizations`](https://site.dev.tunnelhq.com/docs/api/reference/operations/list-organizations/) | Key only | Workspaces the key can reach, with your role in each. |
| [`GET /projects`](https://site.dev.tunnelhq.com/docs/api/reference/operations/list-projects/) | Key only | Projects the key can reach. Filter with `?organization_id=`. |
| [`GET /usage`](https://site.dev.tunnelhq.com/docs/api/reference/operations/get-usage/) | Key only | API request usage and limits for this minute, today, and this month, with daily history and a per-endpoint breakdown. |
| [`GET /keys`](https://site.dev.tunnelhq.com/docs/api/reference/operations/list-keys/) | Key only | List API keys. |
| [`POST /keys`](https://site.dev.tunnelhq.com/docs/api/reference/operations/create-key/) | Key only | Create a workspace API key. Needs a [workspace key](https://site.dev.tunnelhq.com/docs/api-keys/#kinds-of-key); an account-wide key gets `403`. |
| [`DELETE /keys/:id`](https://site.dev.tunnelhq.com/docs/api/reference/operations/delete-key/) | Key only | Disable an API key. Add `?hard=1` to delete it for good. |

These endpoints don't need a key:

| Method and path | Description |
| --- | --- |
| [`GET /health`](https://site.dev.tunnelhq.com/docs/api/reference/operations/get-health/) | Whether the API is up, with its version and uptime. |
| [`GET /health/deep`](https://site.dev.tunnelhq.com/docs/api/reference/operations/get-deep-health/) | Also checks the database and the job queue. Returns `503` if either fails. |
| [`GET /regions`](https://site.dev.tunnelhq.com/docs/api/reference/operations/list-regions/) | The locations you can test from, with the number of healthy checkers in each. |
| [`POST /check/public`](https://site.dev.tunnelhq.com/docs/api/reference/operations/check-config-public/) | A one-off config check, limited to 5 per IP address per 24 hours. Nothing is saved. The [free tester](https://tunnelhq.com/test/) on this site uses it. |

## Monitor status in the API

The API's status words differ from the dashboard's:

| API `status` | Dashboard shows | Meaning |
| --- | --- | --- |
| `healthy` | Healthy | The latest check passed. |
| `down` | Down | Confirmed down. |
| `partial` | Partial | Several locations, some up and some down. |
| `degraded` | Unknown or Degraded | The latest check has no verdict yet (retrying, inconclusive, or a fault on TunnelHQ's checker), shown as **Unknown**; or the server refused the monitor's credentials, shown as **Degraded**. |
| `paused` | Paused | The monitor is paused. |
| `unknown` | Pending | The monitor hasn't been checked yet. |

Per-location results in `regional_status` use their own words: `up`, `down`, `pending`, `degraded`, `unavailable`, and `unknown`. See the [API reference](https://site.dev.tunnelhq.com/docs/api/reference/) for every field.

## Errors

Errors return a JSON body with the HTTP status repeated in `code`:

```json title="404: standard error"
{ "error": true, "code": 404, "message": "Server not found" }
```

Usage-limit errors (`429` when you exceed your plan's request limits) use a different shape, with `error` naming the limit you hit:

| Limit | `error` | `retry_after` |
| --- | --- | --- |
| Per minute | `Rate limit exceeded` | `60` |
| Per day | `Daily limit exceeded` | `3600` |
| Per month | `Monthly limit exceeded` | Not sent |

```json title="429: usage limit"
{ "error": "Rate limit exceeded", "message": "Per-minute limit of 60 requests exceeded", "retry_after": 60 }
```

Too many failed key attempts from one IP address also return `429`, in the standard shape:

```json title="429: too many failed keys"
{ "error": true, "code": 429, "message": "Too many failed API key attempts from this address. Wait a few seconds and try again.", "retry_after": 6 }
```

## Rate limits and usage

Requests are limited per workspace, according to its plan, in fixed per-minute, per-day, and per-month windows.

| Plan | Per minute | Per day | Per month | On-demand tests per month |
| --- | --- | --- | --- | --- |
| Free | — | — | — | 5 |
| Starter | 30 | 5,000 | 50,000 | 50 |
| Pro | 60 | 25,000 | 250,000 | 500 |
| Business | 120 | 100,000 | 1,000,000 | Unlimited |

Free has no API access. The on-demand test quota counts one test per monitor tested, whether from the dashboard, `POST /test`, `POST /test/batch`, or an incident retest. It resets at the start of each calendar month (UTC). `POST /check` doesn't count against it.

Authenticated responses report the per-minute window in three headers:

| Header | Meaning |
| --- | --- |
| `X-RateLimit-Limit` | Requests allowed per minute on your plan. |
| `X-RateLimit-Remaining` | Requests left in the current minute. |
| `X-RateLimit-Reset` | Unix time when the current minute ends. |

When you hit the per-minute limit, the `429` response includes `Retry-After: 60`. For the per-day limit it's `Retry-After: 3600`. The monthly limit has no `Retry-After`.

The **API Keys** page shows usage for each key and for the whole workspace.
