Inkbox

> # Documentation index
> Fetch the complete documentation index at: https://inkbox.ai/sitemap.xml
> Use this file to discover all available pages before exploring further.

---

# Tunnels
description: Give your agent a stable public URL using the Inkbox SDK

---


# Tunnels

A tunnel gives your agent a stable public hostname — `{tunnel_name}.inkboxwire.com` — that routes inbound HTTP, WebSocket, and raw-TCP traffic from third parties to your agent over a single persistent connection. Your agent stays behind whatever NAT, firewall, or laptop Wi-Fi it happens to be running on; no public IP, no firewall hole, no reverse proxy needed on your end.

> Use the Python or TypeScript SDK to create tunnels and bring them online from your agent.

## Creating a tunnel

Pick a name and call `inkbox.tunnels.create(...)`. The response includes a one-time `connect_secret` — store it securely; it is shown only once.

**Python**

```python
created = inkbox.tunnels.create(tunnel_name="my-agent")
print(created.tunnel.id, created.tunnel.public_host)
print(created.connect_secret)  # store this — shown once
```

**TypeScript**

```typescript
const created = await inkbox.tunnels.create({
    tunnelName: "my-agent",
});
console.log(created.tunnel.id, created.tunnel.publicHost);
console.log(created.connectSecret);  // store this — shown once
```

Tunnel names are 3–63 characters, lowercase letters / digits / hyphens, must start and end alphanumeric, no consecutive hyphens, and are globally unique within the environment.

## Connecting your agent

The data-plane `connect()` helper opens the persistent agent connection and forwards inbound traffic to wherever you point it. The simplest setup forwards traffic to a local HTTP server you're already running.

**Python**

```python
# Bring the tunnel online and forward to your local server.
# On first run, the helper creates the tunnel if needed and writes
# the connect secret to ~/.inkbox/tunnels/my-agent/state.json.
listener = inkbox.tunnels.connect(
    name="my-agent",
    forward_to="http://localhost:8080",
)
print(listener.public_url)  # https://my-agent.inkboxwire.com
listener.wait()  # blocks until SIGINT / SIGTERM
```

**TypeScript**

```typescript
// connect() lives on a Node-only subpath because it pulls in
// node:http2, node:tls, and node:fs. The main package entry stays
// browser-safe.

const listener = await connect(inkbox, {
    name: "my-agent",
    forwardTo: "http://localhost:8080",
});
console.log(listener.publicUrl);  // https://my-agent.inkboxwire.com
await listener.wait();  // resolves on clean close, throws on fatal
```

The first call to `connect()` creates the tunnel if it doesn't already exist, persists the connect secret to a local state directory (default `~/.inkbox/tunnels/{name}`), and opens the agent connection. Subsequent calls reuse the existing tunnel and the secret on disk. The TypeScript listener installs SIGINT and SIGTERM handlers automatically only when the parent process has none at construction time, so it stays out of the way of host processes that own their own shutdown — pass `installSignalHandlers: false` (or `true` to attach alongside) to override.

## In-process handlers

If you don't want to run a separate local HTTP server, hand `connect()` a handler function and it will run inside your agent process.

**Python**

```python
# Any ASGI app works — pass the app callable as forward_to.
# Example with FastAPI:
from fastapi import FastAPI

app = FastAPI()

@app.post("/webhook")
async def webhook(payload: dict):
    return {"ok": True, "received": payload}

listener = inkbox.tunnels.connect(name="my-agent", forward_to=app)
listener.wait()
```

**TypeScript**

```typescript
// Pass a Fetch-API handler: (req, ctx) => Response | Promise<Response>.

const listener = await connect(inkbox, {
    name: "my-agent",
    handler: async (req, ctx) => {
        const url = new URL(req.url);
        if (url.pathname === "/webhook" && req.method === "POST") {
            const payload = await req.json();
            return Response.json({ ok: true, received: payload });
        }
        return new Response("not found", { status: 404 });
    },
});
await listener.wait();
```

The `ctx` argument on the TypeScript handler exposes `forwardedForIp`, `sniHost`, an `AbortSignal` that fires when the runtime's deadline expires, and a read-only `envelope` escape hatch for metadata not surfaced on the typed fields.

## WebSockets

Inbound WebSocket upgrades are bridged transparently to your handler.

**Python**

```python
# ASGI handles HTTP and WebSocket through the same app callable.
# Use any ASGI framework that supports WS (FastAPI, Starlette, etc.).
from fastapi import FastAPI, WebSocket

app = FastAPI()

@app.websocket("/ws")
async def ws(socket: WebSocket):
    await socket.accept()
    while True:
        msg = await socket.receive_text()
        await socket.send_text(f"echo: {msg}")

listener = inkbox.tunnels.connect(name="my-agent", forward_to=app)
listener.wait()
```

**TypeScript**

```typescript
// In TypeScript, WebSockets are a separate handler from HTTP.
// You still need an HTTP path (forwardTo or handler) alongside.

const listener = await connect(inkbox, {
    name: "my-agent",
    forwardTo: "http://localhost:8080",  // or handler: ...
    wsHandler: async (ws) => {
        await ws.accept();
        for await (const msg of ws) {
            await ws.send(`echo: ${msg}`);
        }
    },
});
await listener.wait();
```

## Sync vs async lifecycle

`connect()` returns a `TunnelListener`. In Python, pick **one** lifecycle pair and stick with it — sync (`wait` / `close`) and async (`serve_forever` / `aclose`) are mutually exclusive on a given listener. In TypeScript, the listener is always async-driven; `wait()` is the canonical way to block until shutdown.

**Python**

```python
# Sync — for scripts and CLIs. Blocks the calling thread until
# SIGINT / SIGTERM, then drives a clean shutdown.
listener = inkbox.tunnels.connect(name="my-agent", forward_to="http://localhost:8080")
listener.wait()

# Async — when you're already inside an event loop.

async def main():
    listener = inkbox.tunnels.connect(
        name="my-agent",
        forward_to="http://localhost:8080",
    )
    try:
        await listener.serve_forever()
    finally:
        await listener.aclose()

asyncio.run(main())
```

**TypeScript**

```typescript
// listener.wait() resolves on clean close (Ctrl+C / SIGTERM, or
// an explicit close()) and throws if the runtime hit a fatal error
// like a permanently invalid connect secret.
const listener = await connect(inkbox, {
    name: "my-agent",
    forwardTo: "http://localhost:8080",
});

try {
    await listener.wait();
} finally {
    await listener.close();
}

// If your host process owns shutdown, opt out of the auto-installed
// signal handlers and drive close() yourself.
const headless = await connect(inkbox, {
    name: "my-agent",
    forwardTo: "http://localhost:8080",
    installSignalHandlers: false,
});
```

## Rotating the connect secret

If you lose the secret or suspect it's been compromised, rotate it. The new secret takes effect on the agent's next reconnect; existing live connections keep serving traffic until they drop.

**Python**

```python
rotated = inkbox.tunnels.rotate_secret(tunnel_id)
print(rotated.connect_secret)  # store this — shown once
```

**TypeScript**

```typescript
const rotated = await inkbox.tunnels.rotateSecret(tunnelId);
console.log(rotated.connectSecret);  // store this — shown once
```

To cut over hard, restart your agent immediately after rotating.

## Deleting and restoring

`delete()` schedules removal: inbound traffic stops immediately and the tunnel enters a 24-hour grace window during which you can `restore()` it. After 24 hours the tunnel is removed and the name is released for re-use. SDK clients see this state as `TunnelStatus.PENDING_REMOVAL`; the wire shape returned by the REST API is `delete_pending`.

`force_delete()` finalizes a tunnel that's *already* in the grace window — call `delete()` first, then `force_delete()` to skip the remaining 24-hour wait. It returns `409` if the tunnel isn't pending removal, and requires an [admin-scoped API key](/docs/api-keys).

**Python**

```python
# Schedule removal — name is reserved for 24 hours
inkbox.tunnels.delete(tunnel_id)

# Undo within the grace window
inkbox.tunnels.restore(tunnel_id)

# Skip the remaining grace window AFTER delete() (admin-scoped key only)
inkbox.tunnels.delete(tunnel_id)
inkbox.tunnels.force_delete(tunnel_id)
```

**TypeScript**

```typescript
// Schedule removal — name is reserved for 24 hours
await inkbox.tunnels.delete(tunnelId);

// Undo within the grace window
await inkbox.tunnels.restore(tunnelId);

// Skip the remaining grace window AFTER delete() (admin-scoped key only)
await inkbox.tunnels.delete(tunnelId);
await inkbox.tunnels.forceDelete(tunnelId);
```

## Listing and inspecting tunnels

**Python**

```python
# List every tunnel in your org
for t in inkbox.tunnels.list():
    print(t.tunnel_name, t.status, t.currently_connected)

# Fetch one by id, including a live currently_connected flag
t = inkbox.tunnels.get(tunnel_id)

# Tag a tunnel with free-form metadata for your own tracking
inkbox.tunnels.update(
    tunnel_id,
    description="staging webhook receiver",
    metadata={"env": "staging", "owner": "agent-platform"},
)
```

**TypeScript**

```typescript
// List every tunnel in your org
for (const t of await inkbox.tunnels.list()) {
    console.log(t.tunnelName, t.status, t.currentlyConnected);
}

// Fetch one by id, including a live currentlyConnected flag
const t = await inkbox.tunnels.get(tunnelId);

// Tag a tunnel with free-form metadata for your own tracking
await inkbox.tunnels.update(tunnelId, {
    description: "staging webhook receiver",
    metadata: { env: "staging", owner: "agent-platform" },
});
```

`metadata` is a free-form object capped at 4 KB serialized. It's returned as-is on read and is visible only within your org.

## Passthrough TLS

By default, Inkbox terminates TLS at our edge with a managed certificate. If your compliance posture or client-side cert pinning requires TLS to terminate inside your own process, use `tls_mode="passthrough"` and sign a CSR via `inkbox.tunnels.sign_csr(...)`. See the [Passthrough TLS reference](/docs/api/tunnels/passthrough) for the full flow, including how `connect()` handles cert provisioning automatically when you pass `tls_mode="passthrough"`.

## Reference

- [Tunnels API overview](/docs/api/tunnels) — REST endpoint summary
- [Manage tunnels](/docs/api/tunnels/manage) — CRUD endpoint reference
- [Connect secret](/docs/api/tunnels/secret) — rotation endpoint
- [Passthrough TLS](/docs/api/tunnels/passthrough) — CSR signing flow
