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>) |
| Permissions | 0600, owned by you — filesystem permissions are the only access control |
| Transport | plain 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.