Skip to content

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.

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.

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

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

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.

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.

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

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:

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.

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.

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 return a JSON body with the HTTP status repeated in code:

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
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:

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 }

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.