Skip to content

Check a config

POST
/check
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.

Media typeapplication/json
object
config

The share link, config file, or host:port.

string
protocol

Required unless config is a share link.

string
Allowed values: wireguard amneziawg openvpn openconnect ikev2 vless vmess trojan shadowsocks hysteria2 tuic
host

OpenConnect or IKEv2 server; overrides the host in an OpenVPN or WireGuard config.

string
port

Defaults: 443 for OpenConnect, 500 for IKEv2, the remote line’s port for OpenVPN.

integer
username
string
password
string
remote_id

IKEv2 server identity. Defaults to host.

string
timeout

Seconds the test may take. Values outside the range are clamped.

integer
default: 30 >= 5 <= 120
engine

Force the checker’s engine for share links, such as singbox.

string
region

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.

string
default: any /^([a-z]{2}|any)$/
region_fallback

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.

string
default: any
Allowed values: any fail
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"
}

The test ran. status says whether the tunnel came up.

Media typeapplication/json
object
test_id
required

An ID for this check. It can’t be looked up.

string
status
required
string
Allowed values: healthy down
protocol
required
string
Allowed values: wireguard amneziawg openvpn openconnect ikev2 vless vmess trojan shadowsocks hysteria2 tuic
config_detected
required

true when the protocol came from a share link.

boolean
tested_at
required

When the check started, in ISO 8601 UTC.

string
latency_ms
required
integer | null
connection_time_ms
required
integer | null
handshake_time_ms
required
integer | null
assigned_ip
required

When the tunnel came up: its own address if it has one (WireGuard, IKEv2), otherwise the public address traffic left from (share links).

string | null
error_message
required

The checker’s own description of the failure. The wording varies.

string | null
error_category
required

The kind of failure, such as server_not_connected, internet_stuck, timeout, auth_failed, cert_error, or config_error.

string | null
engine_used
required

The engine that ran a share-link check: xray or singbox.

string | null
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"
}
X-RateLimit-Limit
integer

Requests the workspace’s plan allows per minute.

X-RateLimit-Remaining
integer

Requests left in the current minute.

X-RateLimit-Reset
integer

Unix time, in seconds, when the current minute ends.

The config can’t be read, or a field is missing.

Media typeapplication/json
object
error
required
boolean
code
required

The HTTP status, repeated.

integer
message
required

What went wrong.

string
details

More detail, on some errors.

retry_after

Seconds to wait, on the 429 for wrong keys.

integer
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)."
}

The API key is missing, wrong, expired, switched off, or no longer valid.

Media typeapplication/json
object
error
required
boolean
code
required

The HTTP status, repeated.

integer
message
required

What went wrong.

string
details

More detail, on some errors.

retry_after

Seconds to wait, on the 429 for wrong keys.

integer
Examples
{
"error": true,
"code": 401,
"message": "API key is required. Provide X-API-Key header or Authorization: Bearer <key>"
}

Testing from a specific location needs the Pro plan or above.

Media typeapplication/json
object
error
required
boolean
code
required

The HTTP status, repeated.

integer
message
required

What went wrong.

string
details

More detail, on some errors.

retry_after

Seconds to wait, on the 429 for wrong keys.

integer
Example
{
"error": true,
"code": 402,
"message": "Region targeting requires the Pro plan or above. Upgrade to test from \"ae\"."
}

The protocol isn’t supported.

Media typeapplication/json
object
error
required
boolean
code
required

The HTTP status, repeated.

integer
message
required

What went wrong.

string
details

More detail, on some errors.

retry_after

Seconds to wait, on the 429 for wrong keys.

integer
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.

Media typeapplication/json
One of:
object
error
required
string
Allowed values: Rate limit exceeded Daily limit exceeded Monthly limit exceeded
message
required
string
retry_after

Seconds to wait. Absent for the monthly limit.

integer
Examples
{
"error": "Rate limit exceeded",
"message": "Per-minute limit of 60 requests exceeded",
"retry_after": 60
}
Retry-After
integer

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.

Media typeapplication/json
object
error
required
boolean
code
required

The HTTP status, repeated.

integer
message
required

What went wrong.

string
details

More detail, on some errors.

retry_after

Seconds to wait, on the 429 for wrong keys.

integer
Example
{
"error": true,
"code": 500,
"message": "Internal server error"
}

TunnelHQ’s checker couldn’t be reached.

Media typeapplication/json
object
error
required
boolean
code
required

The HTTP status, repeated.

integer
message
required

What went wrong.

string
details

More detail, on some errors.

retry_after

Seconds to wait, on the 429 for wrong keys.

integer
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.

Media typeapplication/json
object
error
required
boolean
code
required

The HTTP status, repeated.

integer
message
required

What went wrong.

string
details

More detail, on some errors.

retry_after

Seconds to wait, on the 429 for wrong keys.

integer
Examples
{
"error": true,
"code": 503,
"message": "No VPN checker nodes available. Try again shortly."
}