Skip to content

API Reference

Tunnel Whisperer exposes two APIs: a REST/WebSocket/SSE API served by the dashboard for browser and HTTP clients, and a gRPC API for CLI-to-daemon communication.


REST API (Dashboard)

The dashboard HTTP server registers the endpoints listed below. All REST endpoints accept and return JSON unless noted otherwise.

Authentication required

Every route except /login, /logout, and /static/* requires the dashboard token — either a SameSite=Strict session cookie (set at login) or an Authorization: Bearer <token> header. /metrics is gated too. Fetch the token with tw dashboard token (see Dashboard → Signing in). Unauthenticated browser requests are redirected to /login; unauthenticated API requests get 401.

Read-only

Method Path Description
GET /api/status Current daemon status (mode, version, relay, server/client state)
GET /api/config Current configuration
GET /api/relay Relay provisioning status (provisioned, domain, IP, provider, ssh_open)
GET /api/providers List of supported cloud providers for relay provisioning
GET /api/stats Bandwidth statistics snapshots and history (returns enabled: false when analytics is off)
GET /metrics Prometheus-format bandwidth metrics

Contexts

Method Path Description
GET /api/config/contexts List stored contexts (name, role, user, relay, id, current)
POST /api/config/use-context Switch the daemon's active context and reconnect. Body: { "name": "..." }

Mode

Method Path Description
POST /api/mode Set the operating mode

Request body:

{ "mode": "server" }

Settings

Method Path Description
POST /api/proxy Set or clear the outbound proxy URL
POST /api/log-level Set the log level (debug, info, warn, error)
POST /api/settings/server Update server settings (ports, relay SSH user, temp Xray port)
POST /api/settings/xray Update Xray transport settings (relay host, port, path)
POST /api/settings/client Update client settings (SSH user, server SSH port, Xray port, listen address)
POST /api/settings/analytics Enable/disable analytics and set history size

Proxy request body:

{ "proxy": "socks5://host:1080" }

Log level request body:

{ "log_level": "debug" }

Server settings request body:

{
  "ssh_port": 2222,
  "api_port": 50051,
  "dashboard_port": 8080,
  "relay_ssh_port": 22,
  "relay_ssh_user": "ubuntu",
  "remote_port": 2222,
  "xray_port": 54001,
  "temp_xray_port": 59000
}

Only non-zero / non-empty fields are applied; omitted fields keep their current value.

Xray settings request body:

{
  "relay_host": "relay.example.com",
  "relay_port": 443,
  "path": "/tw"
}

Client settings request body:

{
  "ssh_user": "tunnel",
  "server_ssh_port": 2222,
  "xray_port": 54001,
  "listen_address": "127.0.0.1"
}

xray_port and listen_address are optional. listen_address sets the local interface forwarded tunnels bind to (0.0.0.0 to expose them on all interfaces).

Analytics settings request body:

{
  "enabled": true,
  "history_size": 720
}

Analytics changes take effect immediately — no restart required. The stats collector is created or destroyed on the fly.

Restart required

Settings changes are persisted to config.yaml immediately. A restart (server) or reconnect (client) is needed for most changes to take effect. Exception: analytics settings take effect immediately.

Server control

Method Path Description
POST /api/server/start Start all server components (SSH, Xray, reverse tunnel)
POST /api/server/stop Stop the server
POST /api/server/restart Stop and restart the server

Client control

Method Path Description
POST /api/client/start Start the client (Xray + SSH tunnel)
POST /api/client/stop Stop the client
POST /api/client/reconnect Disconnect and reconnect the client
POST /api/client/upload Upload a client context bundle (.twctx) to configure the client
POST /api/client/port-override Set or clear a tunnel's local port override, keyed by server port (client mode only; the HTTP counterpart of tw client set-port)

Upload: POST /api/client/upload expects a multipart/form-data body with the bundle in a config file field (10 MB max).

Port override: POST /api/client/port-override takes a JSON body {"server_port": <int>, "local_port": <int>}, or {"server_port": <int>, "clear": true} to remove an override. Takes effect on next reconnect.

Relay management

Method Path Description
POST /api/relay/test-creds Validate cloud provider credentials
POST /api/relay/provision Provision a new relay server via Terraform
POST /api/relay/destroy Destroy the provisioned relay server
POST /api/relay/test Run connectivity tests against the relay
POST /api/relay/generate-script Generate the manual install script for a bring-your-own-VM relay
POST /api/relay/save-manual Save relay details from a manual (non-Terraform) setup
WS /api/relay/ssh WebSocket-based interactive SSH shell to the relay server
POST /api/relay/close-ssh Close the interactive relay SSH session

WebSocket: /api/relay/ssh

This endpoint upgrades to a WebSocket connection and provides a full interactive terminal session to the relay server. The dashboard uses xterm.js to render the terminal in the browser.

Enrolled servers (relay mode only)

These endpoints mirror the tw relay … enrollment commands and answer only when the daemon runs in relay mode.

Method Path Description
GET /api/servers List enrolled servers with live tunnel state (tw relay get-servers)
POST /api/servers/unenroll Un-enroll a server. Body: { "server_id": "..." }

Enrollment has no REST endpoint

tw relay invite and tw join require a spoken SAS confirmation between two humans, which doesn't map to a stateless REST call — there is no /api/servers/enroll. Enroll from the CLI; the dashboard's Servers page only manages already-enrolled tenants.

User management

Method Path Description
GET /api/users List all configured users
DELETE /api/users/{name} Delete a user by name
POST /api/users/{name}/single-session Enable/disable the user's single-session flag
POST /api/users/apply Register users on the relay. Body: { "names": [...] } (empty = all)
POST /api/users/unregister Unregister users from the relay
GET /api/users/online List currently connected users

There is no create or edit endpoint: users come to exist only through the interactive invite ceremony (tw server user invite), and their mappings are fixed at enrollment — changing them means deleting the user and inviting them again.

Application templates

Method Path Description
GET /api/apps List all application templates
POST /api/apps Create a new application template
PUT /api/apps/{name} Update an application template
DELETE /api/apps/{name} Delete an application template

Create/update application request body:

{
  "name": "web-app",
  "mappings": [
    { "client_port": 3000, "server_port": 3000 },
    { "client_port": 5432, "server_port": 5432 }
  ]
}

Application templates are reusable port mapping bundles. They are stored in config.yaml under server.applications and are not synced to the relay.

Server-Sent Events (SSE)

Method Path Description
GET /api/events/{session_id} SSE stream of daemon events (status changes, progress)
GET /api/logs SSE stream of real-time log output

The {session_id} parameter identifies a browser session so multiple dashboard tabs can each receive events independently.

Event format — unnamed data: frames only (no event: field). Each frame is one ProgressEvent (or, on /api/logs, one log entry):

data: {"step":2,"total":5,"label":"Starting Xray","status":"running"}

Daemon status is polled via GET /api/status, not streamed.


gRPC API

The gRPC API listens on 127.0.0.1:50051 (loopback only; port configurable via server.api_port) and is used for CLI-to-daemon communication. It starts automatically with tw server start and tw dashboard. When a daemon is running, CLI commands like tw status and tw server user list connect to this API instead of reading state directly from disk.

Authentication

Loopback is not treated as an auth boundary: every RPC requires a per-daemon bearer token. The daemon generates a 32-byte token into a 0600 file (api.token in the config directory) on startup; the built-in client reads it from the same directory and attaches it as authorization: Bearer <token> metadata. A caller that cannot read the token file gets Unauthenticated, so a local process cannot overwrite keys or config, flip mode, delete users, or read secrets through the API merely by reaching loopback.

JSON codec — the proto is documentation only

The API uses the gRPC server machinery, but the wire format is JSON, not protobuf. A custom codec (registered under the content-subtype json) marshals hand-written Go structs directly, so no protoc-generated code is involved. The file proto/api/v1/service.proto exists as documentation only; make proto regenerates stubs that are not used on the wire. The service is registered as api.v1.TunnelWhisperer and every RPC is unary.

Clients must therefore dial with the JSON call option and attach the bearer token (the built-in client does: grpc.CallContentSubtype("json"), per-RPC token credentials over the plaintext loopback transport, 2-second dial timeout).

Note

The gRPC API is an internal interface. Its message shapes may change between versions. Use the REST API for integrations.

Service: api.v1.TunnelWhisperer

Method Request → Response Description
GetStatus EmptyStatusResponse Mode, version, relay status, user count, connected-user count, server/client component state
GetConfig EmptyConfigResponse The current on-disk configuration
SetMode SetModeRequestEmpty Set the operating mode
ListProviders EmptyListProvidersResponse Supported cloud providers for relay provisioning
GetRelayStatus EmptyRelayStatusResponse Relay provisioning/connection status
TestCredentials TestCredentialsRequestEmpty Validate cloud-provider credentials
ProvisionRelay ProvisionRelayRequestProvisionRelayResponse Provision a relay VM via Terraform
DestroyRelay DestroyRelayRequestEmpty Destroy the relay (accepts cloud credentials map)
TestRelay EmptyTestRelayResponse Run relay connectivity tests; returns per-step results
StartServer / StopServer EmptyEmpty Start/stop all server components
StartClient / StopClient EmptyEmpty Start/stop the client
UploadClientConfig UploadClientConfigRequestEmpty Import a client context bundle (bytes) in client mode
ListUsers EmptyListUsersResponse All configured users with tunnel mappings
DeleteUser DeleteUserRequestEmpty Delete a user by name

The CLI's built-in client wraps the subset it needs: GetStatus, TestRelay, ListUsers, DeleteUser, and DestroyRelay; every CLI command that can use the daemon falls back to local (on-disk) operation when no daemon answers on server.api_port. The invite flows (tw relay invite, tw server user invite, tw join) don't go through this API at all — they dial the relay directly over the Xray/SSH tunnel.