Check a config
const url = 'https://app.tunnelhq.com/api/v1/check';const options = { method: 'POST', headers: {'X-API-Key': '<X-API-Key>', 'Content-Type': 'application/json'}, body: '{"config":"vless://<uuid>@sg-01.your-vpn.net:443?type=tcp&security=reality&sni=www.microsoft.com&pbk=<public-key>#sg-01"}'};
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/check \ --header 'Content-Type: application/json' \ --header 'X-API-Key: <X-API-Key>' \ --data '{ "config": "vless://<uuid>@sg-01.your-vpn.net:443?type=tcp&security=reality&sni=www.microsoft.com&pbk=<public-key>#sg-01" }'Tests a VPN config once, without saving a monitor, and waits for the result. Any key can
call it; no X-Project-Id needed, and it doesn’t use the on-demand test quota.
Send the config in config: a share link for VLESS, VMess, Trojan, Shadowsocks, Hysteria2
and TUIC (the protocol is read from the link), the config file for WireGuard, AmneziaWG and
OpenVPN, or host (or config as host:port) for OpenConnect and IKEv2, which take
username and password too. Unlike Create monitor, this endpoint
reads the share link from config, not url.
Need an API key? See Getting an API key.
Authorizations
Section titled “Authorizations”Request Bodyrequired
Section titled “Request Bodyrequired”object
The share link, config file, or host:port.
Required unless config is a share link.
OpenConnect or IKEv2 server; overrides the host in an OpenVPN or WireGuard config.
Defaults: 443 for OpenConnect, 500 for IKEv2, the remote line’s port for OpenVPN.
IKEv2 server identity. Defaults to host.
Seconds the test may take. Values outside the range are clamped.
Force the checker’s engine for share links, such as singbox.
Where to test from: any, or a lowercase country code such as pk. A country needs the
Pro plan or above. List regions shows the
locations available now.
What happens when the location has no checker available. any runs the test from the
general pool instead, and the response doesn’t say so; fail refuses it. Any other value
counts as any.
Examples
A share link
{ "config": "vless://<uuid>@sg-01.your-vpn.net:443?type=tcp&security=reality&sni=www.microsoft.com&pbk=<public-key>#sg-01"}OpenConnect
{ "protocol": "openconnect", "host": "ams-01.your-vpn.net", "username": "monitor", "password": "<password>"}Responses
Section titled “ Responses ”The test ran. status says whether the tunnel came up.
object
An ID for this check. It can’t be looked up.
true when the protocol came from a share link.
When the check started, in ISO 8601 UTC.
When the tunnel came up: its own address if it has one (WireGuard, IKEv2), otherwise the public address traffic left from (share links).
The checker’s own description of the failure. The wording varies.
The kind of failure, such as server_not_connected, internet_stuck, timeout, auth_failed, cert_error, or config_error.
The engine that ran a share-link check: xray or singbox.
Examples
{ "test_id": "chk_muh251s09f41c07d2e", "status": "healthy", "protocol": "vless", "config_detected": true, "tested_at": "2026-09-25T14:30:00.000Z", "latency_ms": 162, "connection_time_ms": 410, "handshake_time_ms": 120, "assigned_ip": "198.51.100.20", "error_message": null, "error_category": null, "engine_used": "xray"}{ "test_id": "chk_muh25bv27e2290aa13", "status": "down", "protocol": "openconnect", "config_detected": false, "tested_at": "2026-09-25T14:31:00.000Z", "latency_ms": null, "connection_time_ms": null, "handshake_time_ms": null, "assigned_ip": null, "error_message": "handshake timed out", "error_category": "server_not_connected", "engine_used": null}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.
The config can’t be read, or a field is missing.
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": "Could not detect protocol from config. Provide the 'protocol' field explicitly for config-based protocols (wireguard, amneziawg, openvpn, ikev2, openconnect)."}{ "error": true, "code": 400, "message": "Missing or invalid config"}{ "error": true, "code": 400, "message": "Could not extract Endpoint from wireguard config. Ensure config contains [Peer] Endpoint = host:port"}{ "error": true, "code": 400, "message": "Missing host for openconnect. Provide 'host' or a parseable 'config' like \"vpn.example.com:443\"."}{ "error": true, "code": 400, "message": "Invalid region: \"germany\". Use \"any\" or a 2-letter ISO 3166-1 alpha-2 code (lowercase)."}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"}Testing from a specific location needs the Pro plan or above.
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": 402, "message": "Region targeting requires the Pro plan or above. Upgrade to test from \"ae\"."}The protocol isn’t supported.
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": 422, "message": "Unsupported protocol: pptp. Supported: wireguard, openvpn, ikev2, openconnect, vmess, vless, trojan, shadowsocks, amneziawg, hysteria2, tuic"}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"}TunnelHQ’s checker couldn’t be reached.
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": 502, "message": "VPN checker unreachable: timeout of 35000ms exceeded"}No checker is available, or none in the requested location with region_fallback set to fail.
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": 503, "message": "No VPN checker nodes available. Try again shortly."}{ "error": true, "code": 503, "message": "No nodes available for region \"ae\""}