Create monitor
const url = 'https://app.tunnelhq.com/api/v1/servers';const options = { method: 'POST', headers: { 'X-Project-Id': 'prj_12', 'X-API-Key': '<X-API-Key>', 'Content-Type': 'application/json' }, body: '{"name":"fra-wg-01","protocol":"wireguard","config":"[Interface]\nPrivateKey = <client-private-key>\nAddress = 10.0.0.2/32\n\n[Peer]\nPublicKey = <server-public-key>\nEndpoint = fra-wg-01.your-vpn.net:51820\nAllowedIPs = 0.0.0.0/0","tags":["production"]}'};
try { const response = await fetch(url, options); const data = await response.json(); console.log(data);} catch (error) { console.error(error);}curl --request POST \ --url https://app.tunnelhq.com/api/v1/servers \ --header 'Content-Type: application/json' \ --header 'X-API-Key: <X-API-Key>' \ --header 'X-Project-Id: prj_12' \ --data '{ "name": "fra-wg-01", "protocol": "wireguard", "config": "[Interface]\nPrivateKey = <client-private-key>\nAddress = 10.0.0.2/32\n\n[Peer]\nPublicKey = <server-public-key>\nEndpoint = fra-wg-01.your-vpn.net:51820\nAllowedIPs = 0.0.0.0/0", "tags": [ "production" ] }'Creates a VPN monitor or an Uptime check and queues its first check. Needs a role that can create monitors (Owner, Admin, or Manager). A VPN monitor also needs room under the plan’s tunnel limit. Uptime checks don’t count toward it while Uptime is in beta; only a plan that allows no monitors at all refuses them.
The new monitor tests from Any location (choose specific locations in the dashboard), and uses every active alert channel in the project.
The body is one of the shapes below.
- VPN monitors: the configuration fields depend on the protocol.
protocolcan be left out whenurlstarts with a known scheme (vless://,vmess://,trojan://,ss://,hysteria2://,hy2://,tuic://). Hosts in the IANA documentation ranges, such asexample.comor192.0.2.1, are refused with422: use your own server’s address. - Uptime checks: send
type(http,keyword,ping,port,dns, orpushfor a heartbeat) and that type’s fields. A field the type doesn’t have is refused with400and a message naming it, such as “port is for port checks.” Private and reserved addresses are refused."type": "group"is refused with400“Groups are created in the dashboard for now.” ssl_expirationanddomain_expirationleft out take the value of the project’s other checks on the same host, or for the host’s first check the default: 14 days for anhttps://URL, and domain expiration off.- A heartbeat’s
periodbelow the plan’s shortest is refused with403“The shortest period on your plan is N seconds (requested M).” Itsheartbeat_urlis returned only to callers who can edit monitors. Anyone with the address can report the job up or down, so treat it as a secret.
Tags are saved in lowercase.
Need an API key? See Getting an API key.
Authorizations
Section titled “Authorizations”Parameters
Section titled “ Parameters ”Header Parameters
Section titled “Header Parameters”The project, as prj_12 or 12. A workspace key can only reach its own workspace’s projects.
Example
prj_12Request Bodyrequired
Section titled “Request Bodyrequired”object
The check’s name.
Seconds between checks. Refused below the shortest your plan allows. Left out, the recommended default.
Seconds a request may take. See Working with checks for the choices.
Failed checks in a row before the check is down.
false creates it paused.
The address to request.
Status codes that count as up, as codes or ranges.
Certificate: Days before it expires to alert: 1, 2, 3, 7, 14, 30, or 60. 0 or null turns it off. Website and keyword checks only. Needs an https:// URL.
Domain registration: Days before it expires to alert: 1, 2, 3, 7, 14, 30, or 60. 0 or null turns it off. Website and keyword checks only.
null for both.
object
The check’s name.
Seconds between checks. Refused below the shortest your plan allows. Left out, the recommended default.
Seconds a request may take. See Working with checks for the choices.
Failed checks in a row before the check is down.
false creates it paused.
The address to request.
The text to look for in the response.
true alerts when the keyword is found.
object
The check’s name.
Seconds between checks. Refused below the shortest your plan allows. Left out, the recommended default.
Seconds a request may take. See Working with checks for the choices.
Failed checks in a row before the check is down.
false creates it paused.
A public hostname or IP address.
object
The check’s name.
Seconds between checks. Refused below the shortest your plan allows. Left out, the recommended default.
Seconds a request may take. See Working with checks for the choices.
Failed checks in a row before the check is down.
false creates it paused.
A public hostname or IP address.
object
The check’s name.
Seconds between checks. Refused below the shortest your plan allows. Left out, the recommended default.
Seconds a request may take. See Working with checks for the choices.
Failed checks in a row before the check is down.
false creates it paused.
The DNS server: a public IP address, IPv6 with or without brackets. Otherwise 400 “host must be the DNS server’s public IP address, like 1.1.1.1.”
The domain to ask about. Labels may start with _. Otherwise 400 “domain must be a domain name to query, like example.com.”
Text the answer must contain, usually the address the domain should point to.
interval, timeout, and maxretries are refused: a heartbeat’s timing is its period and grace.
object
The longest time between two calls from the job
How late a call can be before the heartbeat is down
object
The monitor’s name.
Seconds between scheduled checks. At least the plan’s minimum: 300 on Starter, 120 on Pro, 60 on Business. Left out, it’s 180 or the plan’s minimum, whichever is longer.
Seconds a check may take. 0 means the default.
Failed checks in a row before the monitor is down. 0 means the default.
false creates the monitor paused. 1, 0, "true" and "false" also work.
Tag names, saved in lowercase. A name the workspace doesn’t have yet creates the tag.
Optional when url starts with a known scheme.
The full share link, such as vless://… or ss://…. uri works as another name for it.
object
The monitor’s name.
Seconds between scheduled checks. At least the plan’s minimum: 300 on Starter, 120 on Pro, 60 on Business. Left out, it’s 180 or the plan’s minimum, whichever is longer.
Seconds a check may take. 0 means the default.
Failed checks in a row before the monitor is down. 0 means the default.
false creates the monitor paused. 1, 0, "true" and "false" also work.
Tag names, saved in lowercase. A name the workspace doesn’t have yet creates the tag.
The full [Interface] and [Peer] config. Checks connect to the Endpoint in [Peer].
Newlines can be sent as \n. wireguard_config works as another name for it.
The server’s address, used only when config has no Endpoint.
object
The monitor’s name.
Seconds between scheduled checks. At least the plan’s minimum: 300 on Starter, 120 on Pro, 60 on Business. Left out, it’s 180 or the plan’s minimum, whichever is longer.
Seconds a check may take. 0 means the default.
Failed checks in a row before the monitor is down. 0 means the default.
false creates the monitor paused. 1, 0, "true" and "false" also work.
Tag names, saved in lowercase. A name the workspace doesn’t have yet creates the tag.
The full AmneziaWG config, with its obfuscation parameters. Required unless servers is sent.
The server’s address, used only when config has no Endpoint.
Several configs in one monitor, instead of config. Each needs wg (the config) and, if it has no Endpoint, ip.
object
object
The monitor’s name.
Seconds between scheduled checks. At least the plan’s minimum: 300 on Starter, 120 on Pro, 60 on Business. Left out, it’s 180 or the plan’s minimum, whichever is longer.
Seconds a check may take. 0 means the default.
Failed checks in a row before the monitor is down. 0 means the default.
false creates the monitor paused. 1, 0, "true" and "false" also work.
Tag names, saved in lowercase. A name the workspace doesn’t have yet creates the tag.
The contents of the .ovpn file.
Overrides the host in the config’s remote line.
Overrides the port in the config’s remote line.
Overrides the config’s proto line.
object
The monitor’s name.
Seconds between scheduled checks. At least the plan’s minimum: 300 on Starter, 120 on Pro, 60 on Business. Left out, it’s 180 or the plan’s minimum, whichever is longer.
Seconds a check may take. 0 means the default.
Failed checks in a row before the monitor is down. 0 means the default.
false creates the monitor paused. 1, 0, "true" and "false" also work.
Tag names, saved in lowercase. A name the workspace doesn’t have yet creates the tag.
The server, such as vpn.your-company.com.
Only if the server doesn’t use its default port.
object
The monitor’s name.
Seconds between scheduled checks. At least the plan’s minimum: 300 on Starter, 120 on Pro, 60 on Business. Left out, it’s 180 or the plan’s minimum, whichever is longer.
Seconds a check may take. 0 means the default.
Failed checks in a row before the monitor is down. 0 means the default.
false creates the monitor paused. 1, 0, "true" and "false" also work.
Tag names, saved in lowercase. A name the workspace doesn’t have yet creates the tag.
The EAP username.
The EAP password.
The server’s identity. Defaults to host.
Only if the server doesn’t use its default port.
Examples
WireGuard
{ "name": "fra-wg-01", "protocol": "wireguard", "config": "[Interface]\nPrivateKey = <client-private-key>\nAddress = 10.0.0.2/32\n\n[Peer]\nPublicKey = <server-public-key>\nEndpoint = fra-wg-01.your-vpn.net:51820\nAllowedIPs = 0.0.0.0/0", "tags": [ "production" ]}Website check
{ "type": "http", "name": "Homepage", "url": "https://example.com", "ssl_expiration": 14}DNS check
{ "type": "dns", "name": "Resolver", "host": "1.1.1.1", "domain": "example.com"}Heartbeat
{ "type": "push", "name": "Nightly backup", "period": 86400, "grace": 3600}VLESS share link
{ "name": "sg-vless-01", "url": "vless://<uuid>@sg-01.your-vpn.net:443?type=tcp&security=reality&sni=www.microsoft.com&fp=chrome&pbk=<public-key>#sg-vless-01"}OpenVPN
{ "name": "nyc-ovpn-01", "protocol": "openvpn", "config": "client\ndev tun\nproto udp\nremote nyc-01.your-vpn.net 1194\nauth-user-pass\n<ca>\n…\n</ca>", "username": "monitor", "password": "<password>", "interval": 300}OpenConnect
{ "name": "ams-ocserv-01", "protocol": "openconnect", "host": "ams-01.your-vpn.net", "username": "monitor", "password": "<password>"}IKEv2
{ "name": "lon-ikev2-01", "protocol": "ikev2", "host": "lon-01.your-vpn.net", "username": "monitor", "password": "<password>"}Responses
Section titled “ Responses ”The new monitor. Its first check is queued, so its status starts as unknown, or paused when it’s created with "active": false.
object
vpn for a tunnel, or the Uptime check’s type (push is a heartbeat).
healthy: the latest check passed. With several locations, at least one is up and none is down.down: confirmed down. With several locations, at least one is down and none is up.partial: several locations, some up and some down.degraded: the latest check has no verdict yet (retrying, inconclusive, or a fault on TunnelHQ’s checker), or the server refused the monitor’s credentials. The dashboard shows the first case as Unknown and the second as Degraded.paused: the monitor is paused.unknown: the monitor hasn’t been checked yet. The dashboard shows it as Pending.
Always the same as status.
false when the monitor is paused.
Seconds between scheduled checks.
The latest check’s time, in milliseconds. For a tunnel, one round trip through the tunnel. For an Uptime check: the response time (website and keyword), the average round trip (ping), the TCP connect time (TCP port), the query time (DNS, null unless the server answered every question), or the ping value the job sent (heartbeat). Null when that check failed or measured none.
Tunnels only: check rows don’t have it. How long the tunnel took to come up in its latest check, in milliseconds. Null when that check failed or measured none. For VLESS, VMess, Trojan, Shadowsocks, Hysteria2, and TUIC, it’s the time to first contact with the server.
Where the latest check ran from, as any or a lowercase country code. null before the first check.
The locations the monitor tests from. ["any"] unless specific locations were chosen.
Locations that are paused. Never includes any.
The latest result per location, keyed by location code. {} before the first check.
object
object
pending means no verdict yet; degraded, that the server refused the credentials;
unavailable, that no checker is available there, so the location is left out of the status.
When that location last checked, in ISO 8601 UTC. Empty if unknown.
Why the check failed, when it did.
Present only when the check ran through a relay.
Whether a partial outage alerts.
Tag names.
When the latest check ran, in UTC, as YYYY-MM-DD HH:MM:SS with no time zone marker.
Present only on monitors imported from a subscription URL. One monitor stands for every config in the URL that points at the same server.
object
The latest result for each config.
object
The config’s name in the subscription.
The config’s protocol, such as vless.
The address traffic left from, when the check passed.
The same fields per location.
object
Uptime checks only: the address a website or keyword check requests.
Uptime checks only: the host a ping or TCP port check reaches, or the DNS server a DNS check asks.
Uptime checks only: the domain a DNS check asks about. null for other checks.
Uptime checks only: the TCP port check’s port, or 53 for DNS.
Uptime checks only: the text a keyword check looks for, or the text a DNS answer must contain.
Uptime checks only: true when a keyword check alerts on finding the keyword.
Uptime checks only: status codes that count as up, such as 200-299.
Uptime checks only: the certificate the latest check saw, or null before one was seen.
object
Uptime checks only: days before the certificate expires to alert. null is off.
Uptime checks only: days before the domain expires to alert. null is off.
Uptime checks only: ipv4 or ipv6, or null for both.
Heartbeats only: the longest time between two calls from the job, in seconds. interval shows the same value.
Heartbeats only: how late a call can be before the heartbeat is down, in seconds.
Heartbeats only, and only from Create, Get, and Update monitor, to callers who can edit monitors: the address the job calls. Treat it as a secret.
Examples
VPN monitor
{ "id": "srv_43", "name": "fra-wg-01", "type": "vpn", "protocol": "wireguard", "status": "unknown", "aggregate_status": "unknown", "active": true, "interval": 180, "latency_ms": null, "connect_ms": null, "region": null, "locations": [ "any" ], "disabled_locations": [], "regional_status": {}, "notify_on_partial": false, "tags": [ "production" ], "last_checked": null}Website check, created paused
{ "id": "srv_210", "name": "Homepage", "type": "http", "protocol": "http", "status": "paused", "aggregate_status": "paused", "active": false, "interval": 180, "latency_ms": null, "region": null, "locations": [ "any" ], "disabled_locations": [], "regional_status": {}, "notify_on_partial": false, "tags": [], "last_checked": null, "url": "https://example.com", "host": null, "domain": null, "port": null, "keyword": null, "invert_keyword": false, "accepted_statuscodes": [ "200-299" ], "certificate": null, "ssl_expiration": 14, "domain_expiration": null, "ip_version": null}Heartbeat, created paused
{ "id": "srv_211", "name": "Nightly backup", "type": "push", "protocol": "push", "status": "paused", "aggregate_status": "paused", "active": false, "interval": 86400, "latency_ms": null, "region": null, "locations": [ "any" ], "disabled_locations": [], "regional_status": {}, "notify_on_partial": false, "tags": [], "last_checked": null, "url": null, "host": null, "domain": null, "period": 86400, "grace": 3600, "port": null, "keyword": null, "invert_keyword": false, "accepted_statuscodes": [ "200-299" ], "certificate": null, "ssl_expiration": null, "domain_expiration": null, "ip_version": null, "heartbeat_url": "https://app.tunnelhq.com/api/push/<token>"}Headers
Section titled “Headers”Requests the workspace’s plan allows per minute.
Requests left in the current minute.
Unix time, in seconds, when the current minute ends.
A field is missing or invalid, or the X-Project-Id header is.
object
The HTTP status, repeated.
What went wrong.
More detail, on some errors.
Seconds to wait, on the 429 for wrong keys.
Examples
{ "error": true, "code": 400, "message": "Missing or invalid name"}{ "error": true, "code": 400, "message": "Missing 'protocol' field. Supported: wireguard, amneziawg, openvpn, openconnect, ikev2, vless, vmess, trojan, shadowsocks, hysteria2, tuic."}{ "error": true, "code": 400, "message": "interval must be a positive integer (seconds)"}{ "error": true, "code": 400, "message": "Maximum check interval is 86400s (requested 90000s)"}{ "error": true, "code": 400, "message": "timeout must be between 10 and 120 seconds (requested 300)"}{ "error": true, "code": 400, "message": "retries must be between 1 and 10 (requested 20)"}{ "error": true, "code": 400, "message": "X-Project-Id header is required (e.g. prj_1 or numeric id)"}The API key is missing, wrong, expired, switched off, or no longer valid.
object
The HTTP status, repeated.
What went wrong.
More detail, on some errors.
Seconds to wait, on the 429 for wrong keys.
Examples
{ "error": true, "code": 401, "message": "API key is required. Provide X-API-Key header or Authorization: Bearer <key>"}{ "error": true, "code": 401, "message": "Invalid or expired API key"}{ "error": true, "code": 401, "message": "API key has expired"}{ "error": true, "code": 401, "message": "API key owner account is inactive"}{ "error": true, "code": 401, "message": "This API key's workspace access has been removed"}The role, the plan, or the key doesn’t allow it.
object
The HTTP status, repeated.
What went wrong.
More detail, on some errors.
Seconds to wait, on the 429 for wrong keys.
Examples
{ "error": true, "code": 403, "message": "You do not have permission to create monitors in this project"}{ "error": true, "code": 403, "message": "Tunnel limit reached (20/20). Upgrade your plan to add more."}{ "error": true, "code": 403, "message": "Your plan does not allow adding monitors. Please upgrade."}{ "error": true, "code": 403, "message": "Minimum check interval on your plan is 300s (requested 60s)"}{ "error": true, "code": 403, "message": "VPN protocol testing is not available on your plan. Upgrade to use this feature."}{ "error": true, "code": 403, "message": "This API key is scoped to a different organization"}{ "error": true, "code": 403, "message": "You do not have access to this project"}No active project has this ID.
object
The HTTP status, repeated.
What went wrong.
More detail, on some errors.
Seconds to wait, on the 429 for wrong keys.
Example
{ "error": true, "code": 404, "message": "Project not found or inactive"}A configuration field is longer than its limit.
object
The HTTP status, repeated.
What went wrong.
More detail, on some errors.
Seconds to wait, on the 429 for wrong keys.
Example
{ "error": true, "code": 413, "message": "Field 'config' exceeds max length (102400 bytes). Got 150000 bytes."}The protocol or its configuration can’t be used.
object
The HTTP status, repeated.
What went wrong.
More detail, on some errors.
Seconds to wait, on the 429 for wrong keys.
Examples
{ "error": true, "code": 422, "message": "Unsupported protocol: pptp. Supported: wireguard, amneziawg, openvpn, openconnect, ikev2, vless, vmess, trojan, shadowsocks, hysteria2, tuic"}{ "error": true, "code": 422, "message": "Missing 'url' for vless — expected a vless:// URI."}{ "error": true, "code": 422, "message": "WireGuard requires a 'config' field containing the full [Interface]/[Peer] config block."}{ "error": true, "code": 422, "message": "OpenVPN requires 'username' and 'password' (even if the config has auth-user-pass, credentials must be provided)."}{ "error": true, "code": 422, "message": "OpenConnect requires host, username, and password."}{ "error": true, "code": 422, "message": "Host 'vpn.example.com' is a reserved documentation address and can never be reached. Use your own server's address."}A usage limit was reached, or this IP address made too many requests with a wrong key.
Usage limits are per workspace and plan: per minute (Retry-After: 60), per day
(Retry-After: 3600), and per month (no Retry-After). They use their own body shape.
An IP address may send 10 wrong keys a minute; one more is allowed every 6 seconds. While
that allowance is used up, every request from the address is refused, even one with a
valid key.
object
Seconds to wait. Absent for the monthly limit.
object
The HTTP status, repeated.
What went wrong.
More detail, on some errors.
Seconds to wait, on the 429 for wrong keys.
Examples
{ "error": "Rate limit exceeded", "message": "Per-minute limit of 60 requests exceeded", "retry_after": 60}{ "error": "Daily limit exceeded", "message": "Daily limit of 25000 requests exceeded", "retry_after": 3600}{ "error": "Monthly limit exceeded", "message": "Monthly limit of 250000 requests exceeded"}{ "error": true, "code": 429, "message": "Too many failed API key attempts from this address. Wait a few seconds and try again.", "retry_after": 6}Headers
Section titled “Headers”Seconds to wait. 60 for the per-minute limit, 3600 for the daily limit, 6 after wrong keys. Absent for the monthly limit.
Something failed on TunnelHQ’s side.
object
The HTTP status, repeated.
What went wrong.
More detail, on some errors.
Seconds to wait, on the 429 for wrong keys.
Example
{ "error": true, "code": 500, "message": "Internal server error"}