tnl.dev :: docs
automation.
Read JSON and NDJSON tunnel state and diagnostics.
follow a publish run
tnl publish exposes a line-oriented event stream; tnl status exposes a point-in-time local snapshot. Use their separate --output formats when writing scripts. tnl dev does not offer machine output; its child output remains unchanged.
tnl publish 3000 --output ndjsonEach stdout line is an independent JSON object with schema_version: 1, a type, a monotonically increasing cursor within this invocation, and a tunnel_id. A typical successful stream looks like this (IDs and addresses are illustrative):
{"schema_version":1,"type":"starting","cursor":1,"tunnel_id":"tunnel_0123456789abcdef0123456789abcdef","target":"http://127.0.0.1:3000"}
{"schema_version":1,"type":"current_ip","cursor":2,"tunnel_id":"tunnel_0123456789abcdef0123456789abcdef","ip":"192.0.2.10"}
{"schema_version":1,"type":"ready","cursor":3,"tunnel_id":"tunnel_0123456789abcdef0123456789abcdef","url":"https://web.alex.example.com","publish_run_number":1}Wait for type: "ready" before handing the url to another program. A later ready event can have a higher publish_run_number after a new publish run; do not assume there is only one. current_ip is omitted when --allow-all-ips is used. Certificate and publisher-connection provisioning does not produce a separate NDJSON event. On a clean cancellation, stopped has reason: "canceled". A warning or error may appear instead; each consumes the next cursor.
The event fields are schema_version, type, cursor, tunnel_id, and optional target, url, publish_run_number, ip, message, code, help_url, retryable, retry_at, reason, and transport. Absent fields are omitted, not emitted as null. A stalled-provisioning warning has a diagnostic code; a QUIC-to-TLS/TCP fallback warning has transport and no diagnostic code. Do not parse human progress frames as NDJSON.
inspect local tunnels
tnl status --output json
tnl status --all --output jsonThe first command covers the current project; --all covers every project in local client state. It does not query the server for every public URL. The JSON result is one object, not a stream:
{
"schema_version": 1,
"observed_at": "2026-09-27T12:00:00Z",
"summary": {
"total": 1,
"starting": 0,
"provisioning": 0,
"ready": 1,
"draining": 0,
"stale": 0
},
"tunnels": [
{
"tunnel_id": "tunnel_0123456789abcdef0123456789abcdef",
"command": "publish",
"state": "ready",
"process_id": 1234,
"server": "https://control.example.com",
"project": "/home/alex/project",
"service": "web",
"public_url_id": "public_url_0123456789abcdef0123456789abcdef",
"publish_run_number": 1,
"hostname": "web.alex.example.com",
"public_url": "https://web.alex.example.com",
"target": "http://127.0.0.1:3000",
"started_at": "2026-09-27T11:59:50Z",
"updated_at": "2026-09-27T11:59:58Z",
"heartbeat_at": "2026-09-27T11:59:58Z",
"lease_expires_at": "2026-09-27T12:00:18Z"
}
]
}summary always contains total, starting, provisioning, ready, draining, and stale. Every tunnel has an ID, command (dev or publish), state, process_id, server, project, and the four timestamps. service, public_url_id, publish_run_number, hostname, public_url, target, and framework appear only when available. Snapshot states are starting, provisioning, ready, draining, and stale; finished tunnels (stopped or failed) do not appear. An empty result has "tunnels": []. A stale local record does not prove that a public URL is currently routable.
handle diagnostics
NDJSON warning and error events include message and retryable; classified diagnostics add code and help_url. retry_at appears only when a rate-limited response supplies a positive retry delay. For example, a target failure represented as a classified error:
{
"schema_version": 1,
"type": "error",
"cursor": 4,
"tunnel_id": "tunnel_0123456789abcdef0123456789abcdef",
"message": "tnl could not reach the local service or get a usable response from it. check that the target is running, then try again.",
"code": "TNL_TARGET_UNAVAILABLE",
"help_url": "https://tnl.dev/e/target",
"retryable": false
}Use code and help_url when available; messages are context, not a parsing contract. The diagnostic catalogue lists the codes and identifies which apply to CLI failures, browser responses, or warnings. A running tunnel may report a TNL_PROVISIONING_STALLED warning while it retries. Not every error has a code. retryable is true for transient control or authority unavailability and rate limits; false is not a signal to ignore the failure. Handle a nonzero process exit as a failure even if the stream ends before a JSON error (for example, invalid arguments before publication starts). Treat unknown codes as failures and use their help URL if present.
The JSON catalogue gives tools the versioned definitions behind these diagnostics.
Public URL responses generated by tnl identify their code in Tnl-Error-Code and return HTML to browsers or plain text to other clients. A response from the local service is forwarded unchanged and has no tnl diagnostic code. Control and authority APIs instead return application/problem+json; their code, status, and short type URL under api problems are a separate contract. Do not match problem titles or messages.