Skip to Content
Teitunnel

Reference

Server API

Automate a Teitunnel server with its JSON API: API keys, every endpoint, the preview → apply flow with fingerprints, change types, errors and security.

teitunnel serve runs a server's tunnels and offers a web dashboard and a small JSON API next to it (see Servers and containers). Use the API to add and remove routes from deploy scripts, read route status and uptime for monitoring, or look at captured requests. Every change goes through the same reviewed plan as in the app: you preview it, then apply exactly that plan.

The machine-readable description (OpenAPI 3.1) is at /api/openapi.json on the server, without signing in.

Start the server

teitunnel serve --set-password        # once: the dashboard password, read from the terminal
teitunnel serve                       # listens on 127.0.0.1:8765
FlagMeaning
--listen ADDRWhere to listen. Default 127.0.0.1:8765.
--allow-remoteAllow a non-loopback address. Put TLS in front of it.
--secure-cookiesMark the session cookie Secure (when served over HTTPS).
--set-passwordSet the dashboard password and exit. TEITUNNEL_WEB_PASSWORD sets it at start instead.
--no-mcpDon't serve the MCP endpoint at /mcp.
--mcp-mode MODEThe MCP endpoint's mode: read-only, ask (default) or full.
--mcp-allow-origin ORIGINA browser origin allowed to call /mcp (repeatable).

serve refuses to start without a password, and refuses a non-loopback address without --allow-remote. Instead of exposing the port, you can also publish the dashboard through Teitunnel itself on a route with a login.

Authentication

Automation uses API keys. Create one on the server and send it as a bearer token:

teitunnel api-key create deploy       # printed once; store it as a secret
teitunnel api-key list                # ids and names only
teitunnel api-key revoke 3            # by id
Authorization: Bearer ttk_…

Keys start with ttk_ and are stored only as hashes, so a lost key can't be shown again: revoke it and create another. The same keys work for /mcp.

The browser dashboard signs in with the password instead: POST /api/login sets a session cookie (HttpOnly, SameSite=Strict, 12 hours). Every change made with a session must also send an X-Teitunnel header (any value), which a cross-site form can't send. API keys don't need it.

Endpoints

MethodPathDoes
GET/api/overviewAccounts, this machine's tunnels and routes with their status, and shares on your domains
POST/api/previewPlans a change. Nothing changes.
POST/api/applyApplies exactly the plan you previewed
GET/api/analyticsEdge traffic for the account's routes on this machine, or one route in detail
GET/api/uptimeUptime of every route on this machine, or one route in detail
GET/api/trafficRequests the inspector captured, newest first
GET/api/traffic/{id}One captured request and its response in full
GET/api/traffic/tapsThe inspector's taps: running in this server, and known from the history
GET/api/openapi.jsonThis API as OpenAPI 3.1 (no sign-in needed)
GET/api/session{"signedIn": true|false} for the dashboard
POST/api/login, /api/logoutThe dashboard's password sign-in ({"password": "…"})
POST, GET, DELETE/mcpThe MCP server for AI agents (Streamable HTTP), with the same API keys: see AI agents

Messages in answers (plan steps, warnings, errors) are plain English text.

GET /api/overview

{
  "machine": "<this machine's name>",
  "accounts": [
    { "id": "…", "name": "<account name>", "overview": { "…": "tunnels, routes, status" }, "error": null }
  ],
  "shares": []
}

An account whose overview couldn't be read has overview: null and the reason in error.

GET /api/analytics

Parameter
accountIdRequired.
hostnameOne route in detail: requests, bytes, status classes and codes, paths, countries, browsers, bots, cache, origin time and a time series. Without it: every route of this machine with requests, error rate, p95 and a sparkline.
pathWith hostname: the route's path rule, e.g. ^/api.
rangehour, day (default), week or month.

Needs Zone · Analytics · Read on the account's token (Analytics). Answers 403 without it, and 429 when Cloudflare's analytics rate limit is reached.

GET /api/uptime

Without parameters: every route on this machine with its uptime over 24 hours, 7 days and 30 days, p95 response time and any open incident. With hostname (and path, range): one route's status strip, response times and incidents; 404 if this machine doesn't serve it.

GET /api/traffic

Requests the inspector captured: shares started over /mcp, and this machine's recent history. Read-only; credentials are always masked, and the API can't reveal them.

Parameter
tapOnly this tap (see /api/traffic/taps).
host, method, pathFilters; path is text in the path.
statusAn exact status (404) or a class (5xx).
textText anywhere in the request (masked secrets can't be searched).
limit1 to 1,000; default 100.
beforeThe previous page's next, for the next page.

The answer has items (id, tap, method, host, path, status, durationMs, sizes, kind, state, webhook) and next. GET /api/traffic/{id} returns one exchange with headers, bodies, timings and the webhook signature check.

Making a change

A change is two calls. The preview returns the plan and its fingerprint; applying sends the same change with that fingerprint. If anything changed in Cloudflare in between, apply answers 409 and nothing is changed: preview again.

Preview

curl -s -X POST http://127.0.0.1:8765/api/preview \
  -H "Authorization: Bearer $TEITUNNEL_API_KEY" -H 'Content-Type: application/json' \
  -d '{"accountId":"<account id>","change":{"type":"addRoute","route":{"hostname":"api.yourhost.com","origin":"8080","path":null,"access":null}}}'
{
  "steps": [
    { "kind": "putConfig", "description": "…", "command": null },
    { "kind": "createRecord", "description": "…", "command": null }
  ],
  "warnings": [],
  "requiresConfirmation": false,
  "fingerprint": "…"
}

steps is empty when there's nothing to change. requiresConfirmation is true when the change would replace or delete DNS records Teitunnel didn't create; applying then needs "confirmed": true.

Apply

curl -s -X POST http://127.0.0.1:8765/api/apply \
  -H "Authorization: Bearer $TEITUNNEL_API_KEY" -H 'Content-Type: application/json' \
  -d '{"accountId":"<account id>","change":{…the same change…},"fingerprint":"…","confirmed":false}'

The outcome has a type:

  • applied: every step succeeded. verify lists hostnames to check; connectorError is set if the connector couldn't be started (the routes are configured).
  • rolledBack: a step failed and everything done before it was undone. failedStep and error say which and why.
  • partiallyApplied: a step failed and some earlier changes couldn't be undone; leftovers lists them.

Get the account id from /api/overview or teitunnel accounts --json.

Change types

change is an object with a type. Fields are camelCase.

typeFields
addRouteroute: { hostname, origin, path, access, options }
updateRoutehostname, path (the route's current ones), route (the new definition)
removeRoutehostname, path
createTunnelname
removeTunnelnone; with tunnelId to choose which
balanceRoute, unbalanceRoutehostname
addNetwork, removeNetworknetwork, e.g. 192.168.1.0/24
importRoutesroutes: a list of route objects
reserveHostnamehostname, until (2026-12-31 or 2026-12-31T18:00Z; null for no end)
releaseHostnamehostname
protectHostnamehostname, protection (below)
restoreConfignone: undo an outside edit of this machine's routes
removeLogindomain: a login Teitunnel added whose route is gone
deleteRecordzoneId, hostname, recordId: an orphaned record Doctor found

A route:

  • hostname: on one of the account's domains.
  • origin: a port ("3000"), host:port, or a URL ("http://localhost:3000").
  • path: a path regex such as "^/api", or null.
  • access: { "emails": ["team@yourhost.com"], "emailDomains": ["yourhost.com"] } to require a login, or null. Add "github": ["teispace/Developers"] for GitHub teams and "signIn": "github" or "google" to allow only that login method. On updateRoute, null removes the login Teitunnel added.
  • options: origin settings (the same as the CLI's origin flags, camelCase), or leave it out for cloudflared's defaults (on updateRoute, leaving it out keeps the current ones).

protection for protectHostname (Edge protection); an empty object removes Teitunnel's rules:

{
  "bots": "challenge",
  "aiCrawlers": true,
  "rateLimit": { "requests": 100, "period": 60, "action": "block" },
  "requestHeaders": [{ "name": "Cookie", "op": "remove" }],
  "responseHeaders": [{ "name": "X-Robots-Tag", "op": "set", "value": "noindex" }],
  "bypassCache": false
}

bots is off, challenge or block; bypassCache: true stops Cloudflare caching the hostname's responses (it needs the Cache Rules permission); period is one of 10, 60, 120, 300, 600 or 3600 seconds; action is block or challenge; op is set, add (response headers only) or remove.

tunnelId (next to accountId) picks one of the machine's tunnels; without it, the default tunnel, or the tunnel carrying an existing route.

Errors

Errors are JSON: { "error": "…" }.

StatusWhen
400The request or change isn't valid, or planning failed (the message says why)
401Invalid API key. or Sign in first.
403Missing the X-Teitunnel header. (a session change without it), or a missing analytics permission
404A route or captured request this machine doesn't have
409Something changed in Cloudflare since this was reviewed. Review it again.
429Too many attempts. Try again in a few minutes. (5 failed sign-ins from one address in 5 minutes), or Cloudflare's analytics limit
502Cloudflare didn't answer an analytics query

Security

  • Listens on loopback unless --allow-remote. No CORS headers are ever sent.
  • Every response carries a strict Content Security Policy, X-Frame-Options: DENY, Referrer-Policy: no-referrer and Cache-Control: no-store.
  • No secret is ever returned: not tokens, not keys, not captured credentials.
  • /mcp refuses requests with an Origin header unless it's allowed with --mcp-allow-origin, accepts only a loopback Host unless the server listens remotely, and refuses repeated bad keys from one address for a while.
Edit on GitHub

Last updated on

On this page