Skip to content
Sign up
Letta Agent SDK
Remote client API

Remote client API

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 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.

A remote environment has three pieces:

PieceMeaning
DeviceThe machine, VM, container, or sandbox where Letta Code runs tools.
ConnectionThe current online WebSocket session for that device. Identified by connectionId.
ClientYour 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-..."
}
}
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.

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.

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.

{
"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.

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);
}
});

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