Guides, Nov 10, 2026

# Test VPN configs from code: API, CLI and MCP

Yes. TunnelHQ's REST API has POST /check, which takes a WireGuard or OpenVPN config, a share link such as vless://, or IKEv2 and OpenConnect credentials, runs a real connection test on TunnelHQ's checkers and saves nothing. The thq CLI and the MCP tool test\_vpn call the same endpoint, so a script, a CI job and an AI assistant all get the same answer. It needs the Starter plan or above.

Tahir Mehmood, 8 min read

If you ship VPN configs, sooner or later you want to test them from something other than a client app: a script that runs every hour, a CI job that refuses a broken config, or an AI assistant you can ask "does this link work?". This post shows all three, with the real requests, what comes back, and the limits.

Everything here is the production API at `https://app.tunnelhq.com/api/v1`. The examples read the key from an environment variable; [Getting an API key](https://tunnelhq.com/docs/api-keys/) shows where to create one.

## Is there an API to test a VPN config?

Yes. `POST /check` takes a config or share link in the request body, runs a connection test, and returns the result. It doesn't need a monitor, a project, or anything saved first.

It's easy to pick the wrong endpoint, because there are two that test:

| Endpoint | Tests | Needs |
| --- | --- | --- |
| `POST /check` | A config, link or set of credentials in the request | An API key |
| `POST /test` | A monitor you already saved, by its `server_id` | An API key and an `X-Project-Id` header |

Use `/check` for one-off tests and pipelines. Use `/test` when you already monitor the server and want a fresh result for it.

A share link is the simplest case, because the protocol comes from the scheme:

```bash
curl -X POST https://app.tunnelhq.com/api/v1/check \
  -H "X-API-Key: $TUNNELHQ_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"config": "vless://<uuid>@vpn.example.com:443?security=reality&...", "timeout": 30}'
```

The schemes it recognises on its own are `vless://`, `vmess://`, `trojan://`, `ss://`, `hysteria2://`, `hy2://` and `tuic://`. For anything else, send `protocol` too. A WireGuard config is multi-line text, so let `jq` build the JSON instead of escaping newlines by hand:

```bash
jq -n --rawfile c wg0.conf '{protocol: "wireguard", config: $c}' |
  curl -X POST https://app.tunnelhq.com/api/v1/check \
    -H "X-API-Key: $TUNNELHQ_API_KEY" \
    -H "Content-Type: application/json" \
    -d @-
```

The same works for `amneziawg` and for an `.ovpn` file with `"protocol": "openvpn"`. IKEv2 and OpenConnect don't need a config at all: send `protocol`, `host`, `username` and `password`, plus `remote_id` for IKEv2 if it differs from the host.

The optional fields are `timeout` (seconds, 5 to 120, default 30), `port`, `engine` to force one, and `region` with `region_fallback` to test from a chosen location, which needs the Pro or Business plan.

Two things to know before you script it. The request stays open until the test finishes, and a dead server takes longer to judge than a working one, so give your HTTP client a timeout well above the test's own. And the test runs from TunnelHQ's checkers on the public internet, so the server has to be reachable from there.

## How do I test a share link from the command line?

Install `thq` and run `thq test` with a file, a link, a raw config, or a subscription URL. It calls `POST /check` for you and works out the protocol on its own.

```bash
curl -fsSL https://tunnelhq.com/install.sh | bash
thq login
```

The installer needs nothing installed first on macOS, Linux or WSL. On Windows, `irm https://tunnelhq.com/install.ps1 | iex` in PowerShell does the same. `thq login` opens the browser to approve the sign-in, with no key to copy.

Then test:

```bash
thq test ./wg0.conf
thq test ./office.ovpn
thq test 'vless://<uuid>@vpn.example.com:443?security=reality&...'
thq test https://provider.example.com/sub --first 5
thq test --protocol ikev2 --host vpn.example.com -u alice -p secret
```

`thq` picks the protocol from `--protocol`, then the link's scheme, then the content (an `[Interface]` section is WireGuard, or AmneziaWG when it has keys such as `Jc` or `H1`; `client` and `remote` lines are OpenVPN), then the file extension. A subscription URL is fetched on your machine, and its entries are tested five at a time. `--first` and `--index` pick a few of them.

Add `--json` for machine-readable output: one JSON object for a single test, or one per entry, as each finishes, for a subscription.

## How do I test configs in CI before shipping?

Run `thq test` in the job with an API key in `THQ_API_KEY`. It exits with `0` when the test passes and `1` when it fails, so a broken config fails the build.

For a single config or link, that's all you need. For a subscription URL, read the exit code carefully: `thq test` exits `1` only when no entry at all is healthy. If you need every config to pass, test them one by one and fail on the first error, or check each JSON line yourself.

A GitHub Actions job that tests every `.conf` and `.ovpn` file in a `configs/` folder:

```yaml
name: Test VPN configs
on: [push, pull_request]

jobs:
  test-configs:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Install thq
        run: |
          curl -fsSL https://tunnelhq.com/install.sh | bash
          echo "$HOME/.local/bin" >> "$GITHUB_PATH"
      - name: Test every config
        env:
          THQ_API_KEY: ${{ secrets.THQ_API_KEY }}
        run: |
          failed=0
          for f in configs/*.conf configs/*.ovpn; do
            [ -e "$f" ] || continue
            thq test "$f" --json || { echo "FAILED: $f"; failed=1; }
          done
          exit $failed
```

Store the key as a repository secret, never in the repo. A key created in the dashboard is tied to its workspace, and you can give it an expiry and revoke it on its own, which is what you want for CI. The key from `thq login` is for your own machine.

Mind the rate limit when the folder is large. Every `thq test` is one API request, and Starter allows 30 a minute, so a big batch on Starter can get a `429` partway through. Split it across runs, or move to a plan with a higher limit.

## How do I let an AI assistant test VPN links?

Connect the TunnelHQ MCP server to the assistant. Then you ask in plain words, and it calls `test_vpn` for one config or `test_batch` for many, both of which run the same check as `POST /check`.

There are two ways to connect, and both need a workspace on Starter or above:

-   **Sign in, no key.** Claude, Claude Code, ChatGPT, Cursor, VS Code and Windsurf connect to `https://mcp.tunnelhq.com/mcp`. You sign in to TunnelHQ and choose a workspace, a project, and **Read only** or **Read and write**. Testing needs **Read and write**. Neither kind of connection can delete anything.
-   **Run it locally with a key.** For clients that start a local server, such as Cline, the server runs with `npx` and a `TUNNELHQ_API_KEY` in the client's config.

For Claude Code, the sign-in way is one command, then `/mcp` and **Authenticate** in the session:

```bash
claude mcp add --transport http tunnelhq https://mcp.tunnelhq.com/mcp
```

The local way, with a key, is easiest through the CLI, which writes the config for you:

```bash
thq mcp --client claude --apply --api-key "thk_..."
```

The same command works with `--client cursor`, `windsurf` or `cline`. The [MCP docs](https://tunnelhq.com/docs/mcp/) have the steps for every client, and the [AI assistants page](https://tunnelhq.com/ai/) shows what an assistant can do once it's connected.

Prompts that call the testing tools:

-   "Test this link and tell me if it connects: vless://..."
-   "Test the first 10 servers in this subscription and list the ones that fail, with the reason."
-   "These three WireGuard configs: which one has the lowest latency?"

`test_batch` takes up to 500 inputs in one call: links, subscription URLs, and with the local server, file and folder paths on your machine. It returns a result for each entry and a summary with the healthy count and the fastest configs. That's cheaper than the assistant calling `test_vpn` again and again.

One caution: the assistant sees whatever you paste, private keys and passwords included. Paste configs only into an assistant you'd trust with them, and use test peers where you can.

## What do the results look like?

Every test returns one JSON object. `status` is `healthy` or `down`, the timing fields are in milliseconds, and on a failure `error_message` and `error_category` say why.

A passing test from `POST /check` (values are examples):

```json
{
  "test_id": "…",
  "status": "healthy",
  "protocol": "vless",
  "config_detected": true,
  "tested_at": "2026-11-11T09:12:44.517Z",
  "latency_ms": 157,
  "connect_ms": 364,
  "connection_time_ms": 412,
  "handshake_time_ms": 298,
  "assigned_ip": null,
  "exit_ip": "198.51.100.7",
  "error_message": null,
  "error_category": null,
  "engine_used": "xray"
}
```

The fields worth using:

| Field | Meaning |
| --- | --- |
| `status` | `healthy` when the tunnel came up and traffic went through it, otherwise `down` |
| `connect_ms` | How long the tunnel took to come up |
| `latency_ms` | One round trip through the tunnel, once it's up |
| `exit_ip` | The address traffic left from, on a pass |
| `error_message`, `error_category` | Why it failed, on a failure |
| `engine_used` | Which engine ran the test |

`thq test --json` prints this same object, so a script can read `status` and `latency_ms` from either.

The checks go past "the port answered". A healthy result means the tunnel came up and real traffic went through it. Two failures you'll see often: `server_not_connected`, where the tunnel never came up, and `internet_stuck`, where the tunnel came up but traffic didn't reach the internet. The second usually means the server's forwarding, NAT or DNS, not the client config.

## What are the rate limits?

Requests are limited per workspace, by plan: 30 a minute on Starter, 60 on Pro and 120 on Business. The Free plan has no API access, so the API, the CLI and the MCP server all start at Starter.

| Plan | Per minute | Per day | Per month |
| --- | --- | --- | --- |
| Starter | 30 | 5,000 | 50,000 |
| Pro | 60 | 25,000 | 250,000 |
| Business | 120 | 100,000 | 1,000,000 |

A `POST /check` counts as one request against these limits. It doesn't use the monthly allowance of on-demand tests, which is for re-testing saved monitors through `POST /test`. When you go over, the API returns `429` with `retry_after`: 60 seconds for the per-minute limit, 3600 for the per-day one. Every response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset`, so a script can slow down before it hits the wall.

Without an account, the [free tester](https://tunnelhq.com/test/) on this site runs the same kind of test in the browser: 5 tests per IP address per 24 hours, no sign-up, and nothing is stored. It's fine for checking a link by hand, and not meant for scripts.

When a test now and then isn't enough, and you want the same check every few minutes, with alerts and history, save the config as a monitor instead. The [REST API docs](https://tunnelhq.com/docs/api/) and the [CLI docs](https://tunnelhq.com/docs/cli/) cover creating monitors from the same configs, and [pricing](https://tunnelhq.com/pricing/) lists the check intervals per plan.

Source: https://tunnelhq.com/blog/test-vpn-configs-api-cli-mcp/
