---
title: Remote client API | Letta Docs
description: Low-level remote-environment transport used by custom Letta Code clients.
---

The Remote client API connects custom clients to a Letta Code remote environment through Letta’s hosted router. It carries the same v2 runtime protocol that the environment serves directly in App Server mode and is one of the underlying interfaces managed by the [Letta Agent SDK](/agent-sdk/index.md).

For most applications, use the Agent SDK instead. With `backend: "cloud"`, the SDK manages the remote environment or cloud sandbox, connects to the Remote client API when needed, sends turns, streams events, and cleans up session state for you.

Use this API directly only when you are building a custom client, dashboard, router, or SDK-level integration that needs to own the remote WebSocket connection.

## How it fits together

A remote environment has three pieces:

| Piece      | Meaning                                                                             |
| ---------- | ----------------------------------------------------------------------------------- |
| Device     | The machine, VM, container, or sandbox where Letta Code runs tools.                 |
| Connection | The current online WebSocket session for that device. Identified by `connectionId`. |
| Client     | Your UI, app server, or router that sends commands and renders events.              |

A device has a stable `deviceId`. A connection is temporary: it changes whenever the remote environment reconnects. Clients use the current `connectionId` to open a hosted status WebSocket.

Most scoped commands include the runtime you want to control:

```
{
  "runtime": {
    "agent_id": "agent-...",
    "conversation_id": "conv-..."
  }
}
```

## Hosted flow

### 1. Find an online environment

Terminal window

```
curl -sS 'https://api.letta.com/v1/environments?onlineOnly=true' \
  -H "Authorization: Bearer $LETTA_API_KEY"
```

Response:

```
{
  "connections": [
    {
      "connectionId": "conn-...",
      "deviceId": "device-abc123",
      "connectionName": "Work laptop",
      "currentMode": "standard",
      "metadata": {
        "workingDirectory": "/workspace/project",
        "gitBranch": "main"
      }
    }
  ],
  "hasNextPage": false
}
```

Pick an online connection and keep its `connectionId`.

### 2. Open the WebSocket

```
wss://api.letta.com/v1/environments/{connectionId}/status/ws?agentId={agentId}&conversationId={conversationId}&channel=stream
```

Authenticate with `Authorization: Bearer <token>` when your WebSocket client supports headers. Browser clients cannot set WebSocket headers, so browser integrations can pass `token=` in the URL instead.

### 3. Sync state

Send `sync` when the socket opens or reconnects:

```
{
  "type": "sync",
  "runtime": {
    "agent_id": "agent-...",
    "conversation_id": "conv-..."
  },
  "recover_approvals": true
}
```

The runtime responds with state events such as `update_device_status`, `update_loop_status`, and `update_queue`.

### 4. Send a turn

```
{
  "type": "input",
  "runtime": {
    "agent_id": "agent-...",
    "conversation_id": "conv-..."
  },
  "payload": {
    "kind": "create_message",
    "messages": [
      {
        "role": "user",
        "content": "What files should I inspect first?",
        "client_message_id": "cm_01J_demo"
      }
    ]
  }
}
```

Listen for `stream_delta` events for output and `update_loop_status` events for turn state. The turn is idle again when the runtime reports `WAITING_ON_INPUT` or emits a stop reason.

## Minimal client

```
const token = process.env.LETTA_API_KEY!;
const connectionId = "conn-...";
const agentId = "agent-...";
const conversationId = "conv-...";


const url = new URL(
  `wss://api.letta.com/v1/environments/${connectionId}/status/ws`,
);
url.searchParams.set("agentId", agentId);
url.searchParams.set("conversationId", conversationId);
url.searchParams.set("channel", "stream");


const ws = new WebSocket(url, {
  headers: { Authorization: `Bearer ${token}` },
});


function send(frame: unknown) {
  ws.send(JSON.stringify(frame));
}


ws.addEventListener("open", () => {
  const runtime = { agent_id: agentId, conversation_id: conversationId };


  send({ type: "sync", runtime, recover_approvals: true });
  send({
    type: "input",
    runtime,
    payload: {
      kind: "create_message",
      messages: [{ role: "user", content: "Summarize this repo." }],
    },
  });
});


ws.addEventListener("message", (event) => {
  const frame = JSON.parse(event.data);


  if (typeof frame.seq === "number") {
    send({ type: "ack", seq: frame.seq });
  }


  if (frame.type === "stream_delta") {
    renderDelta(frame.delta);
  }


  if (frame.type === "update_loop_status") {
    renderStatus(frame.loop_status);
  }
});
```

## Protocol references

The Remote client API shares the same command and event vocabulary as App Server. Use these references instead of duplicating protocol details here:

- [Remote client API reference](/agent-sdk/remote-client/reference/index.md) - Hosted environment discovery, auth, WebSocket URL, and transport ACKs.
- [App Server protocol lifecycle](/platform/app-server/protocol-lifecycle/index.md) - Canonical `input`, `sync`, `abort_message`, streaming, approvals, and device capability commands.
- [Custom remote transport](/agent-sdk/remote-client/self-hosted/index.md) - Connect a runtime to your own router instead of the hosted environment router.
