Search docs

Search the Beamd documentation

Agent local API

The background agent exposes a small HTTP API over a unix socket — the stable v1 contract the CLI itself uses.

Detached tunnels are owned by a background process — the agent. It holds the long-lived connection to the edge and exposes a small HTTP API over a unix domain socket, so the CLI and other local programs can drive it. This is the same API beamd open -d / close / list / status use under the hood.

Most callers should just shell out to the CLI (beamd open <port> --as <name> -d --json, beamd close <name> --json, …) — it spawns the agent on demand and prints clean JSON. Talk to the socket directly only to avoid the process hop or to reach an endpoint the CLI doesn't surface. Treat the shapes below as a stable v1 contract.

The socket

Path~/.beamd/agent.sock (override with the agent's --socket <path>)
Permissions0600, owned by you — filesystem permissions are the only access control
Transportplain HTTP/1.1 over the unix socket (no TLS, no in-band auth)

Lifecycle. The agent is spawned on demand the first time you run beamd open <port> -d. The read-only commands (list, close, status) and a direct socket connection do not start it — if no agent is running, there are simply no detached tunnels. To guarantee one exists, run a detached open once, or run beamd agent as a service. Foreground tunnels live in their own process and are not visible here.

Endpoints

POST /open

Bring a port up as a public URL (idempotent per name).

// request
{ "port": 3000, "name": "api" }
// response 200
{ "url": "https://api.turing.tunnel.example.com", "name": "api", "port": 3000, "slug": "turing", "baseDomain": "tunnel.example.com" }

name is optional (defaults to the port). Errors: 400 (bad JSON / port out of range), 502 (edge rejected — e.g. name_taken, over_limit).

POST /close

Tear down a tunnel by name. Idempotent.

// request
{ "name": "api" }
// response 200
{ "removed": true }

removed is false when no tunnel by that name was present (still a success).

GET /list

[ { "name": "api", "port": 3000, "url": "https://api.turing.tunnel.example.com", "healthy": true } ]

healthy reflects whether the agent currently has a live session to the edge.

GET /healthz

{ "status": "ok", "slug": "turing", "healthy": true }

Error shape

Any non-200 response carries:

{ "error": "human-readable message" }

Node example

const http = require("http")
const path = require("path")
const os = require("os")
 
const socketPath = path.join(os.homedir(), ".beamd", "agent.sock")
 
function call(method, urlPath, body) {
  return new Promise((resolve, reject) => {
    const data = body ? JSON.stringify(body) : null
    const req = http.request(
      { socketPath, method, path: urlPath, headers: data
        ? { "content-type": "application/json", "content-length": Buffer.byteLength(data) }
        : {} },
      (res) => {
        let buf = ""
        res.on("data", (c) => (buf += c))
        res.on("end", () => {
          const parsed = buf ? JSON.parse(buf) : null
          if (res.statusCode >= 400) reject(new Error(parsed?.error ?? `HTTP ${res.statusCode}`))
          else resolve(parsed)
        })
      },
    )
    req.on("error", reject)
    if (data) req.write(data)
    req.end()
  })
}
 
// Ensure the agent exists first (spawns it, returns immediately):
//   $ beamd open 3000 --as api -d --json
const tunnel = await call("POST", "/open", { port: 3000, name: "api" })
console.log(tunnel.url)
await call("POST", "/close", { name: "api" })

To bundle the binary and manage its lifecycle from your own app, see Embed in your app.