Documentation Index
Fetch the complete documentation index at: https://docs.qa.tech/llms.txt
Use this file to discover all available pages before exploring further.
The Remote Tunnels API enables you to programmatically create secure tunnels that expose local ports via Cloudflare. This allows QA.tech to access locally-running applications for testing without deploying them to a public server.
- Base URL:
https://api.qa.tech/v1
- Authentication: Bearer token (project API token)
- Content-Type:
application/json
Prerequisites
Before using the Remote Tunnels API, you need:
-
cloudflared – Cloudflare’s tunnel client must be installed on your machine or CI runner
# macOS
brew install cloudflared
# Linux (Debian/Ubuntu)
curl -L --output cloudflared.deb https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb
sudo dpkg -i cloudflared.deb
# See https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/installation/
-
Your application running locally – The tunnel exposes local ports, so your app must be running
Tunnel Expiration
Tunnels expire 12 hours after creation. The expiresAt field in the response indicates when the tunnel will be automatically torn down. For long-running development, you’ll need to create a new tunnel when the current one expires.
When to Use Remote Tunnels
- Local development testing – Test your local environment without deploying
- CI/CD pipelines – Expose ephemeral test servers during build processes
- Preview environments – Create temporary public URLs for testing
- Firewall-protected environments – Access applications behind corporate firewalls
For CLI-based tunnel management, see the tunnel command documentation. The CLI provides a simpler interface for common tunnel operations.
Authentication
Include your project’s API token in the Authorization header:
Authorization: Bearer YOUR_API_TOKEN
Find your API token in the QA.tech dashboard: Settings → Integrations → API.
Create Remote Tunnel
Create a new tunnel that exposes one or more local ports via Cloudflare. Returns a tunnelToken to start the cloudflared daemon.
Endpoint: POST /remote-tunnels
Request Body
| Field | Type | Required | Description |
|---|
ports | array | Yes | Array of port mappings to expose (see below) |
public | boolean | No | If true, tunnel is publicly accessible without auth |
Port Mapping Fields
| Field | Type | Required | Description |
|---|
localPort | integer | Yes | Local port number to expose |
protocol | string | No | Protocol: "http" (default) or "https" |
subdomain | string | No | Optional subdomain label (e.g. "api" → r-{id}-api.quack.run) |
Response (200)
{
"runnerId": "tunnel_abc123",
"hostnames": [
{
"localPort": 3000,
"url": "https://r-tunnel_abc123-app.quack.run"
},
{
"localPort": 8080,
"url": "https://r-tunnel_abc123-api.quack.run"
}
],
"tunnelToken": "eyJhIjoiYWJjMTIzLi4uIiwidCI6Ii4uLiJ9",
"expiresAt": "2026-05-04T22:30:00.000Z"
}
Response Fields
| Field | Type | Description |
|---|
runnerId | string | Tunnel identifier for subsequent API calls |
hostnames | array | Public hostnames mapped to local ports |
tunnelToken | string | Token to start the cloudflared daemon |
expiresAt | string | ISO 8601 timestamp when the tunnel expires |
Hostname Fields
| Field | Type | Description |
|---|
localPort | integer | Local port this hostname maps to |
url | string | Public HTTPS URL for this port |
Example: Single Port
curl -X POST "https://api.qa.tech/v1/remote-tunnels" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"ports": [
{ "localPort": 3000 }
]
}'
Example: Multiple Ports with Subdomains
curl -X POST "https://api.qa.tech/v1/remote-tunnels" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"ports": [
{ "localPort": 3000, "subdomain": "app" },
{ "localPort": 8080, "subdomain": "api", "protocol": "http" }
]
}'
Starting the Tunnel
After creating a tunnel, start the cloudflared daemon with the returned token:
cloudflared tunnel run --token YOUR_TUNNEL_TOKEN
The tunnel will remain active as long as cloudflared is running and has not expired (12-hour maximum lifetime).
For simpler tunnel management, consider using the CLI tunnel command which handles cloudflared automatically: qatech tunnel start --port 3000
List Remote Tunnels
List all active tunnels for your project.
Endpoint: GET /remote-tunnels
Response (200)
{
"tunnels": [
{
"runnerId": "tunnel_abc123",
"hostnames": [
{ "localPort": 3000, "url": "https://r-tunnel_abc123.quack.run" }
],
"createdAt": "2026-05-04T10:30:00.000Z",
"expiresAt": "2026-05-04T22:30:00.000Z",
"isExpired": false
},
{
"runnerId": "tunnel_def456",
"hostnames": [
{ "localPort": 8080, "url": "https://r-tunnel_def456.quack.run" }
],
"createdAt": "2026-05-03T14:00:00.000Z",
"expiresAt": "2026-05-04T02:00:00.000Z",
"isExpired": true
}
]
}
Response Fields
| Field | Type | Description |
|---|
tunnels | array | Array of tunnel summary objects |
Tunnel Summary Fields
| Field | Type | Description |
|---|
runnerId | string | Tunnel identifier |
hostnames | array | Public hostnames for the tunnel |
createdAt | string | ISO 8601 creation timestamp |
expiresAt | string | ISO 8601 expiration timestamp |
isExpired | boolean | Whether the tunnel has expired |
Example
curl "https://api.qa.tech/v1/remote-tunnels" \
-H "Authorization: Bearer YOUR_API_TOKEN"
Get Tunnel Status
Check the live health status of a tunnel via Cloudflare.
Endpoint: GET /remote-tunnels/{runnerId}/status
Path Parameters
| Parameter | Type | Required | Description |
|---|
runnerId | string | Yes | Tunnel identifier |
Response (200)
{
"status": "healthy",
"connections": 4,
"hostnames": [
{ "localPort": 3000, "url": "https://r-tunnel_abc123.quack.run" }
]
}
Response Fields
| Field | Type | Description |
|---|
status | string | "inactive", "degraded", or "healthy" |
connections | integer | Number of active Cloudflare edge connections |
hostnames | array | Public hostnames for the tunnel |
Status Values
| Status | Connections | Description |
|---|
inactive | 0 | No active connections |
degraded | 1-3 | Tunnel is running but not optimal |
healthy | 4+ | Tunnel is fully operational |
Example
curl "https://api.qa.tech/v1/remote-tunnels/tunnel_abc123/status" \
-H "Authorization: Bearer YOUR_API_TOKEN"
Delete Remote Tunnel
Tear down a tunnel and remove its DNS records.
Endpoint: DELETE /remote-tunnels/{runnerId}
Path Parameters
| Parameter | Type | Required | Description |
|---|
runnerId | string | Yes | Tunnel identifier |
Response (200)
Example
curl -X DELETE "https://api.qa.tech/v1/remote-tunnels/tunnel_abc123" \
-H "Authorization: Bearer YOUR_API_TOKEN"
Complete Workflow Example
Here’s a complete example of creating a tunnel, using it for testing, and cleaning up:
# 1. Create the tunnel
TUNNEL_RESPONSE=$(curl -s -X POST "https://api.qa.tech/v1/remote-tunnels" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"ports": [{"localPort": 3000}]}')
RUNNER_ID=$(echo "$TUNNEL_RESPONSE" | jq -r '.runnerId')
TUNNEL_TOKEN=$(echo "$TUNNEL_RESPONSE" | jq -r '.tunnelToken')
PUBLIC_URL=$(echo "$TUNNEL_RESPONSE" | jq -r '.hostnames[0].url')
echo "Tunnel created: $PUBLIC_URL"
# 2. Start cloudflared in the background
cloudflared tunnel run --token "$TUNNEL_TOKEN" &
CLOUDFLARED_PID=$!
# 3. Wait for tunnel to become healthy
sleep 5
STATUS=$(curl -s "https://api.qa.tech/v1/remote-tunnels/$RUNNER_ID/status" \
-H "Authorization: Bearer YOUR_API_TOKEN" | jq -r '.status')
echo "Tunnel status: $STATUS"
# 4. Run tests against the tunnel URL
curl -X POST "https://api.qa.tech/v1/run" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"testPlanShortId\": \"pln_abc123\",
\"applications\": [{
\"applicationShortId\": \"app_gXeBl2\",
\"environment\": {
\"url\": \"$PUBLIC_URL\",
\"name\": \"Local Tunnel\"
}
}]
}"
# 5. Clean up
kill $CLOUDFLARED_PID
curl -X DELETE "https://api.qa.tech/v1/remote-tunnels/$RUNNER_ID" \
-H "Authorization: Bearer YOUR_API_TOKEN"
Error Responses
| Status | Description |
|---|
| 400 | Validation error or malformed request |
| 401 | Missing or invalid API key |
| 403 | Invalid token or organization suspended |
| 404 | Tunnel or project not found |
| 500 | Server error |
Error responses include a body: { "message": "Error description" }.