Skip to content

Run a test

POST
/test
curl --request POST \
--url https://app.tunnelhq.com/api/v1/test \
--header 'Content-Type: application/json' \
--header 'X-API-Key: <X-API-Key>' \
--header 'X-Project-Id: prj_12' \
--data '{ "server_id": "srv_42", "region": "pk" }'

Tests one monitor now, outside its schedule, and waits up to 75 seconds for the result. Needs a role that can edit monitors (Owner, Admin, or Manager) and uses one on-demand test from the plan’s monthly quota.

If the result arrives in time, the response is 200 with it. Otherwise it’s 202 with status: "pending": the test keeps running, and its result appears in the monitor’s results (List a monitor’s results) and status. test_id can’t be looked up through the API.

Need an API key? See Getting an API key.

X-Project-Id
required
string

The project, as prj_12 or 12. A workspace key can only reach its own workspace’s projects.

Example
prj_12
Media typeapplication/json
object
server_id
required

The monitor, as srv_42 or 42.

string | integer
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
Example
{
"server_id": "srv_42",
"region": "pk"
}

The test finished within 75 seconds.

Media typeapplication/json
object
test_id
required
string
server_id
required
string
status
required

degraded means the test had no verdict.

string
Allowed values: healthy down degraded
latency_ms
required

One round trip through the tunnel in this test, as stored with its result, in milliseconds. Null when the test failed or measured none.

integer | null
connect_ms

How long the tunnel took to come up in this test, as stored with its result, in milliseconds. Null when the test failed or measured none. For VLESS, VMess, Trojan, Shadowsocks, Hysteria2, and TUIC, it’s the time to first contact with the server.

integer | null
error_message
required
string | null
tested_at
required

When it finished, in UTC, as YYYY-MM-DD HH:MM:SS with no time zone marker.

string
Example
{
"test_id": "manual-42-1790346600000",
"server_id": "srv_42",
"status": "healthy",
"latency_ms": 157,
"connect_ms": 364,
"error_message": null,
"tested_at": "2026-09-25 14:30:41"
}
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 test is still running after 75 seconds.

Media typeapplication/json
object
test_id
required
string
server_id
required
string
status
required
string
Allowed value: pending
tested_at
required

When the test started, in ISO 8601 UTC.

string
Example
{
"test_id": "manual-42-1790346600000",
"server_id": "srv_42",
"status": "pending",
"tested_at": "2026-09-25T14:30:00.000Z"
}
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.

server_id or region is invalid, or the X-Project-Id header is.

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": "Missing or invalid server_id"
}

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

The role or the quota doesn’t allow it, or the key can’t reach the project.

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": 403,
"message": "This action requires edit permission — your role is read-only"
}

The project has no such monitor, or the project doesn’t exist.

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": 404,
"message": "Server not found"
}

The tunnel is paused, so there’s nothing to check. The answer comes at once.

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": 409,
"message": "The tunnel is paused. Resume it to run a check."
}

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"
}