Skip to main content

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:
  1. 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/
    
  2. 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

FieldTypeRequiredDescription
portsarrayYesArray of port mappings to expose (see below)
publicbooleanNoIf true, tunnel is publicly accessible without auth

Port Mapping Fields

FieldTypeRequiredDescription
localPortintegerYesLocal port number to expose
protocolstringNoProtocol: "http" (default) or "https"
subdomainstringNoOptional 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

FieldTypeDescription
runnerIdstringTunnel identifier for subsequent API calls
hostnamesarrayPublic hostnames mapped to local ports
tunnelTokenstringToken to start the cloudflared daemon
expiresAtstringISO 8601 timestamp when the tunnel expires

Hostname Fields

FieldTypeDescription
localPortintegerLocal port this hostname maps to
urlstringPublic 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

FieldTypeDescription
tunnelsarrayArray of tunnel summary objects

Tunnel Summary Fields

FieldTypeDescription
runnerIdstringTunnel identifier
hostnamesarrayPublic hostnames for the tunnel
createdAtstringISO 8601 creation timestamp
expiresAtstringISO 8601 expiration timestamp
isExpiredbooleanWhether 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

ParameterTypeRequiredDescription
runnerIdstringYesTunnel identifier

Response (200)

{
  "status": "healthy",
  "connections": 4,
  "hostnames": [
    { "localPort": 3000, "url": "https://r-tunnel_abc123.quack.run" }
  ]
}

Response Fields

FieldTypeDescription
statusstring"inactive", "degraded", or "healthy"
connectionsintegerNumber of active Cloudflare edge connections
hostnamesarrayPublic hostnames for the tunnel

Status Values

StatusConnectionsDescription
inactive0No active connections
degraded1-3Tunnel is running but not optimal
healthy4+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

ParameterTypeRequiredDescription
runnerIdstringYesTunnel identifier

Response (200)

{
  "deleted": true
}

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

StatusDescription
400Validation error or malformed request
401Missing or invalid API key
403Invalid token or organization suspended
404Tunnel or project not found
500Server error
Error responses include a body: { "message": "Error description" }.