REST API
The REST API lets you create and manage VPN monitors, run tests, read results and incidents, and manage subscription URLs. Alert channels, outbound webhooks, team members, billing, and status pages are managed in the dashboard.
All endpoints are under https://app.tunnelhq.com/api/v1.
Authentication
Section titled “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. API access needs the Starter plan or above.
Example requests
Section titled “Example requests”Monitors are exposed as the /servers resource, with prefixed IDs such as srv_916.
List monitors
Section titled “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
Section titled “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.
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
Section titled “Run an on-demand test”POST /test runs a check against an existing monitor and waits up to 75 seconds for the result.
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:
{ "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
Section titled “Endpoints”Each endpoint links to its page in the 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 |
Project | List monitors. |
POST /servers |
Project | Create a monitor. |
GET /servers/:id |
Project | Get a monitor. |
PUT /servers/:id |
Project | Update a monitor. |
DELETE /servers/:id |
Project | Delete a monitor. |
GET /servers/search?q= |
Key only | Search monitors across every project the key can reach. |
POST /test |
Project | Run an on-demand test of one monitor. |
POST /test/batch |
Project | Run on-demand tests of several monitors. |
POST /check |
Key only | Check a config without saving a monitor. |
GET /results |
Project | Recent check results. |
GET /results/failures |
Project | Recent failed checks. |
GET /results/server/:id |
Project | Check history for one monitor. |
GET /incidents |
Project | List incidents. |
GET /incidents/:id |
Project | Get an incident. |
GET /incidents/:id/events |
Project | An incident’s activity log. |
POST /incidents/:id/acknowledge |
Project | Acknowledge an incident. |
POST /incidents/:id/retest |
Project | Retest the monitor behind an incident. |
GET /subscriptions |
Project | List subscription URLs. |
POST /subscriptions |
Project | Add a subscription URL. |
GET /subscriptions/:id |
Project | Get a subscription. |
PUT /subscriptions/:id |
Project | Update a subscription. |
DELETE /subscriptions/:id |
Project | Remove a subscription. |
POST /subscriptions/:id/sync |
Project | Sync a subscription now. |
GET /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 |
Key only | Workspaces the key can reach, with your role in each. |
GET /projects |
Key only | Projects the key can reach. Filter with ?organization_id=. |
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 |
Key only | List API keys. |
POST /keys |
Key only | Create a workspace API key. Needs a workspace key; an account-wide key gets 403. |
DELETE /keys/:id |
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 |
Whether the API is up, with its version and uptime. |
GET /health/deep |
Also checks the database and the job queue. Returns 503 if either fails. |
GET /regions |
The locations you can test from, with the number of healthy checkers in each. |
POST /check/public |
A one-off config check, limited to 5 per IP address per 24 hours. Nothing is saved. The free tester on this site uses it. |
In the dashboard, the API reference shows most endpoints with example requests and responses.
Monitor status in the API
Section titled “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 for every field.
Errors
Section titled “Errors”Errors return a JSON body with the HTTP status repeated in code:
{ "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 |
{ "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:
{ "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
Section titled “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.