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

The Remote client API is the low-level WebSocket transport for controlling a Letta Code remote environment.

For most applications, use the [Letta Agent SDK](/letta-agent-sdk/quickstart/index.md) instead. With `backend: "cloud"`, the SDK manages the remote environment or Constellation sandbox, connects to the Remote client API, 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](/letta-agent/remote-client-api-reference/index.md) - Hosted environment discovery, auth, WebSocket URL, and transport ACKs.
- [App Server protocol lifecycle](/letta-agent/app-server/protocol-lifecycle/index.md) - Canonical `input`, `sync`, `abort_message`, streaming, approvals, and device capability commands.
- [Self-hosted remotes](/letta-agent/remote-client-byor/index.md) - Direct custom transport when you do not want the hosted environment router.
