Remotes API

Register machines that connect as remotes, inspect their status, and dispatch command executions with streamed output.

Requires API key scopes remotes:read (list, get, read exec output) and remotes:write (register, update, delete, heartbeat, WebSocket connect, dispatch execs, signal runs). Launching a harness chat also needs agents:execute.

Who can do what. A remote is its owner's machine, and whatever reaches it runs as the owner's account. Teammates see it through its visibility. Beyond that it is the owner's unless they share it:

ActionWho
See it in listings and get itAnyone its visibility admits
Operate it: dispatch and signal execs, launch and list harness sessions, place agents on itThe owner, or a use or write share
Manage it: update, rename, update belt, policy rules, attachments, delete, sharesThe owner, or a write share
Heartbeat and the daemon WebSocketThe owner only

A caller who can read the remote but lacks the grant gets 403 ("this remote is another person's machine and is not shared with you for this"). Anyone else gets 404. Workspace and org admins do not bypass use or write. Set remote tags is the exception: it is gated by manage_policy.

→ Remotes guide · CLI setup


Concepts

ResourceDescription
RemoteA machine running belt remote start. It hosts agent harness profiles and optional shell or harness-session execution.
ProfileOne harness slot on a remote (for example Claude Code). Profiles are created from daemon registration. There is no REST endpoint that creates them.
Exec runOne command executed on a remote, with sequenced stdout/stderr output stored for replay

The daemon registers with POST /remotes, then keeps an outbound WebSocket open at wss://api.inference.sh/ws/remotes/{id} (or your configured API host). The API treats that connection as the source of truth for status; heartbeats only update heartbeat_at. Command output arrives over that socket as remote_exec_output frames and is stored per run.

Exec opt-in. exec_enabled on the remote reflects whether the daemon was started with belt remote start --exec. The API refuses dispatch when it is false, and the daemon refuses the command frame as well.

Agent sessions opt-in. Registration may include agents_enabled: true when the daemon was started with belt remote start --agents. The daemon enforces this flag: without it, harness session frames are refused on the host even if the API sends them.

Connecting a machine only registers harness profiles on the remote — it does not create workspace agents. The first time you launch a harness chat for a profile, the API creates one agent for that profile (for example title Claude Code · work-laptop) and reuses it on later launches. Run behavior is documented under Remote harness runs.


Register remote

POST /remotes

Called by belt remote start on startup. Registration keys on the daemon's public_key for your user and active team (from the API key or X-Team-ID). A machine that registers again with the same key in the same team reuses its existing remote and does not create a duplicate. Registering after a team switch creates a new remote for that team; older team rows age out through the heartbeat staleness sweep (disconnected, then stopped). Each registration also reconciles the harnesses probe results into profiles (one per harness kind) and returns the remote with profiles populated.

Request

FieldTypeDescription
namestringDisplay name, sent only with --name. Empty keeps the remote's current name; a new remote is named after system_info.hostname.
public_keystringBase64 ed25519 public key that identifies the machine
remote_versionstringbelt version string
system_infoobjectHost metadata (see below)
exec_enabledbooleanWhether this daemon accepts command execution (belt remote start --exec)
agents_enabledbooleanWhether this daemon hosts harness sessions (belt remote start --agents)
homestringOptional. Absolute POSIX home directory of the OS user the daemon runs as (for example /home/you or /Users/you). The API stores it for workspace policy: paths written as ~/… in rules or in remote exec/harness calls resolve against this directory on that machine. Windows-style or relative values are ignored. Not returned on RemoteDTO; re-registration replaces the stored value. Current belt daemons send it on each register.
harnessesarrayHarnesses discovered on the host (see below)

The handler does not validate any of these fields as required.

system_info

belt remote sends a subset of the API's SystemInfo object (same JSON shape engines use for host telemetry). Remotes omit engine-only fields such as gpus, docker, engine_version, and hf_cache. Omitted fields default to zero values server-side.

FieldTypeDescription
hostnamestringMachine hostname
osstringOS label, for example Ubuntu 24.04 LTS or macOS
cpusarrayOne aggregate entry with name, cores, optional frequency, and usage (0–100, all cores)
ramobjecttotal and used in bytes; usage is used memory as a percentage (0–100)
volumesarrayOptional. Today one entry: the filesystem that holds the user's home directory (name is the home path; size, used, free, and usage in bytes / percent)
json
1{2  "hostname": "work-laptop",3  "os": "macOS",4  "cpus": [{ "name": "Apple M3 Pro", "cores": 12, "usage": 14.2 }],5  "ram": { "total": 38654705664, "used": 19327352832, "usage": 50.0 },6  "volumes": [7    { "name": "/Users/you", "size": 994662584320, "used": 512000000000, "free": 482662584320, "usage": 51.5 }8  ]9}

Registration includes a full snapshot with the usage fields above. While the daemon stays connected, it attaches a fresh snapshot to every third WebSocket remote_heartbeat frame (~15 seconds at the default 5 second heartbeat interval). The first heartbeat after connect omits system_info because registration already sent one; CPU usage on later snapshots reflects load since the previous collection. The API stores each heartbeat payload on the remote row, so GET /remotes/{id} can show live utilization without re-registering.

The HTTP POST /remotes/{id}/heartbeat endpoint records liveness only; it does not accept system_info.

harnesses entries

Each object describes one agent harness binary found on PATH:

FieldTypeDescription
kindstringHarness registry id (for example claude, codex, gemini, cursor)
commandstringResolved path to the harness binary
versionstringHarness version from discovery. Optional, best effort.
logged_inbooleantrue when the harness looks configured on the host, or when a usage probe reports authenticated; false when the probe reports unauthenticated. The daemon never reads credential files.
capabilitiesobjectSession capabilities from the daemon's session driver for this harness (see Profile). Omitted when the harness cannot be driven from inference.
accountobjectWho the harness is signed in as (see Profile account). Omitted until a usage probe has run; registration does not wait for one.
limitsobjectPlan rate-limit windows (see Profile limits). Omitted until a usage probe has run.

A harness kind that is missing from the array is not installed, or has no resolvable binary on PATH.

Profile reconciliation

On each registration, and on each remote_heartbeat that carries harnesses, the API reconciles harnesses into the remote's profiles:

InputProfile effect
New kindCreates a profile with harness_kind, command, and status
Existing kindUpdates command, status, and capabilities in place (same profile id)
logged_in: truestatus becomes idle
logged_in: falsestatus becomes unavailable
Harness no longer reportedThe profile is marked unavailable. It is not deleted.
capabilities omittedKeeps the profile's previous capabilities (older daemons)
account or limits presentStored on the profile. A probe that failed (limits.unavailable.reason: probe_failed) keeps the last good windows.

A connected daemon started with --agents rediscovers its harnesses, probes each one's account and plan limits (in parallel, 60 second timeout each) and sends the result on a heartbeat right after it connects and then every 5 minutes.

The API never receives harness credentials.

Response

RemoteDTO:

FieldTypeDescription
idstringRemote ID
namestringDisplay name
statusstringLifecycle status (see below)
heartbeat_atstring | nullTime of the last heartbeat
system_infoobject | nullHost metadata from the daemon (registration and optional WebSocket heartbeat updates)
remote_versionstringbelt version the daemon reported
exec_enabledbooleanWhether the host opted in to command execution
agents_enabledbooleanWhether the host opted in to harness agent sessions
tagsstring[]Policy grouping labels. A workspace rule selector tag:<name> applies to every remote that carries <name>. Omitted or empty when unset.
profilesarrayHarness profiles (see below)
last_updateobjectThe last update the remote was asked for and how it went. Omitted when never asked.

public_key is not part of the response.

Profile

Each ProfileDTO in profiles:

FieldTypeDescription
remote_idstringOwning remote
harness_kindstringHarness registry id (claude, codex, ...)
display_namestringProduct name from the harness registry (for example Claude Code)
vendorstringVendor name from the registry
namestringUser label for this slot on the remote
commandstringHarness binary path on the remote
argsstring[]Extra launch arguments
statusstringidle, busy, or unavailable
max_concurrentintegerConcurrency cap for this profile
capabilitiesobjectWhat hosted sessions can do (steer, approvals, interrupt, resume, tools, images, files). Omitted when not reported yet; treat as all false.
accountobjectWho the harness is signed in as, from the daemon's last probe or a running session. Omitted until known.
limitsobjectPlan rate-limit windows at checked_at. Omitted until known.
Profile account
FieldTypeDescription
statusstringauthenticated, unauthenticated or unknown
typestringsubscription, api_key or cloud
planstringVendor plan id, for example max
plan_labelstringDisplay name, for example Max 20x
emailstringAccount email, when the harness reports one
workspacestringOrganization or workspace name, when reported
Profile limits
FieldTypeDescription
checked_atstring (RFC 3339)When the windows were read
windowsarrayOne entry per rate-limit window
unavailableobjectWhy there are no windows: reason (unsupported or probe_failed) and optional message

Each window:

FieldTypeDescription
idstringStable id per harness, for example five_hour or seven_day
kindstringsession, weekly, monthly or other
labelstringDisplay name
used_percentnumber0 to 100
resets_atstring (RFC 3339)When the window resets; omitted when unknown
duration_minutesintegerWindow length, when known

A running harness session can report a newer account and limits between probes.

Profile statusMeaning
idleReady to take a run
busyAt its concurrency limit
unavailableLogged out, removed from the host, rate-limited, or disabled

Remote lifecycle status:

statusMeaning
pendingRegistered, not yet connected
runningThe daemon holds an open WebSocket to the API
disconnectedThe WebSocket closed. Reversible when the daemon reconnects.
stoppedFinalized after staying disconnected for about 30 seconds

Connection vs heartbeat. status follows the daemon's WebSocket connection, not heartbeat age. Opening the socket moves the remote to running from pending, disconnected, or stopped. Closing it (or losing the connection without a clean close) moves a running remote to disconnected. The API reconciles connection leases about every 10 seconds as a safety net: a remote that drops off presence for two rounds is marked disconnected; one that is present but not running is revived.

heartbeat_at is last seen only. WebSocket and HTTP heartbeats stamp it; they do not change status. Use it for display ("last seen") and for live system_info updates on the WebSocket path.

While disconnected, dispatch and harness launch return 409 remote_offline because the daemon is not connected. Exec runs and harness sessions already running on the host keep going through a brief disconnect (for example Wi‑Fi blip). They fail only when the remote reaches stopped — after the disconnect grace — or when you cancel them.

Harness version support

Each profile's support object describes how the installed version compares to versions inference has tested for that harness. The API computes it from the registry when serializing profiles — it does not run the harness binary.

support.levelMeaning
supportedInside the tested range
newer-than-testedNewer than anything tested. Sessions still run; the UI may warn.
older-than-testedOlder than the tested range but above the driver's minimum. Sessions still run; the UI may warn.
older-than-supportedBelow a floor the session driver needs (for example a missing RPC or app-server API). Launch and new turns are refused until you upgrade on the host.
unknownVersion could not be parsed, the harness is not in the registry, or inference has not published a tested range yet. Sessions may still run; treat as a warning.
FieldDescription
reasonHuman-readable sentence explaining the verdict
versionParsed dotted version from the daemon's --version output
tested_min, tested_maxOldest and newest versions inference has tested for this harness
requiresMinimum version the driver depends on, when defined
upgrade_cmdShell command to install the latest release over the one on the machine (from the registry)

The workspace Remotes machine page maps support onto each launcher card: version beside the vendor; yellow warnings for newer-than-tested, older-than-tested, and unknown on ready cards (sessions still start); a too old pill with reason and a copyable upgrade_cmd when level is older-than-supported (no launch until you upgrade on the host). API clients should mirror that split — warn on untested builds, block launch when below the driver floor. When level is older-than-supported, expect 409 harness_too_old from Launch harness session and 409 conflict from Send chat message on harness-backed chats — both include reason and how to upgrade. The remote daemon applies the same floor before opening a session: it re-reads the installed version at launch (so upgrades since registration count) and refuses with reason and upgrade_cmd when the CLI is still below requires (for example Pi < 0.80.4, Codex < 0.56.0).


List remotes

GET /remotes or POST /remotes/list

Cursor-paginated list of remotes. Requires remotes:read.

bash
1curl -X POST https://api.inference.sh/remotes/list \2  -H "Authorization: Bearer inf_your_key" \3  -H "Content-Type: application/json" \4  -d '{"limit": 20}'

Get remote

GET /remotes/{id}

Returns a RemoteDTO with profiles included. Requires remotes:read. Use the id that belt remote start prints when it connects, or one from the list endpoint.


List harness sessions

GET /remotes/{id}/sessions

Asks the connected daemon which harness sessions exist on the machine — ones inference started and ones a person started locally — and whether a process holds each session right now. Use the result to offer continue on an idle session or watch an active one. Requires remotes:read.

The handler forwards remote_sessions_list over the remote WebSocket and waits for remote_sessions_listed (up to 10 seconds). The remote must be running and registered with agents_enabled: true (belt remote start --agents).

Query

ParameterDescription
cwdOptional. When set, only sessions whose working directory matches are returned (same filter as the WebSocket cwd field).
bash
1curl "https://api.inference.sh/remotes/remote_abc123/sessions?cwd=/home/you/project" \2  -H "Authorization: Bearer inf_your_key"

Response

Same shape as the WebSocket remote_sessions_listed frame:

FieldTypeDescription
request_idstringCorrelates with the daemon request
sessionsarrayHarness sessions on disk (metadata only; no transcript bodies). At most 500 entries, newest first.
errorsarrayOptional per-harness store read failures

Each sessions[] entry:

FieldTypeDescription
harnessstringHarness id (for example claude, codex)
idstringHarness session id on disk (pass as resume_session_id when launching)
cwdstringWorking directory, when known
titlestringDisplay title from the harness store, when known
updatedstringLast metadata update (RFC 3339)
liveobjectWhether a process holds the session now (see below)

live (SessionLiveness):

FieldTypeDescription
statestringactive, idle, or unknown
evidencestringWhat decided liveness — for example lock-file, no-process, process-in-cwd, recent-write, or none
heuristicbooleantrue when liveness is inferred rather than proven (for example from process cwd or recent writes)
pidintegerOS process id, when known
detailstringExtra context from the harness transcript layer

Errors

HTTPCodeCause
409agents_disabledThe daemon was not started with --agents
409remote_offlineThe remote is not connected
504remote_timeoutThe daemon did not answer within 10 seconds

Launch harness chat

POST /remotes/{id}/sessions

Opens a new chat with a harness on a connected remote: pick a profile, set the working directory on that machine, and optionally resume a session the harness already has (from List harness sessions). Requires remotes:write and agents:execute.

The remote must be running with agents_enabled: true (belt remote start --agents). The profile must belong to that remote and report non-null capabilities (the daemon can drive the harness).

Request

FieldTypeRequiredDescription
profile_idstringYesProfile id from GET /remotes/{id} → profiles[].id
work_dirstringNoFolder on the remote where the harness session runs. Leading and trailing whitespace is trimmed. Empty uses the daemon default. Stored on the chat as work_dir (folder-bound sessions; see Get chat).
resume_session_idstringNoHarness session id from a list entry's id field — continues that session instead of starting fresh. Sets harness_session_id on the new chat.
bash
1curl -X POST "https://api.inference.sh/remotes/remote_abc123/sessions" \2  -H "Authorization: Bearer inf_your_key" \3  -H "Content-Type: application/json" \4  -d '{5    "profile_id": "prof_xyz789",6    "work_dir": "/home/you/projects/api",7    "resume_session_id": "sess_abc"8  }'

Response

Returns a ChatDTO (same shape as Get chat) with agent_id set to the profile's harness agent, work_dir from the request, and harness_session_id when resume_session_id was sent. Send the first user message with POST /agents/run, POST /chats/{id}/messages, or open the chat in the workspace.

The API creates the backing agent on first launch for that profile (harness set to the profile's registry id, profile_id and remote_id bound to the machine). Reconnecting the daemon does not duplicate agents.

Errors

HTTPCodeCause
400invalid_requestMissing profile_id or invalid JSON body
404profile_not_foundprofile_id is not on this remote
409agents_disabledThe daemon was not started with --agents
409harness_not_drivableThe profile has no session capabilities (harness not driveable from inference)

List harness catalog

GET /harnesses

Returns every agent harness inference can drive on a remote, from the agentprotocol harness registry. The list includes only registry entries that have a session driver (for example Claude Code, Codex, Pi, and ACP-backed CLIs). Registry entries with no driver — IDE-only agents and backends that only describe a harness — are omitted.

Use this to build launch UIs: show the full set of driveable harness kinds, then match each entry's id to GET /remotes/{id} → profiles[].harness_kind to show which ones that machine installed and whether each profile is idle.

Requires remotes:read.

bash
1curl https://api.inference.sh/harnesses \2  -H "Authorization: Bearer inf_your_key"

Response

An array of catalog entries:

FieldTypeDescription
idstringHarness registry id (claude, codex, gemini, …)
display_namestringProduct name from the registry
vendorstringVendor name
driverstringHow the daemon drives sessions: claude-code, codex, pi, or acp
capabilitiesobjectDriver defaults for this harness kind: steer, approvals, interrupt, resume, tools

capabilities here come from the driver implementation, not from a specific machine. Per-host behavior and login state still come from daemon registration and each profile's capabilities and status.


Update remote

POST /remotes/{id}

Updates a remote you own and returns the updated RemoteDTO. Requires remotes:write.

The body is a full remote object. The update writes every mutable field, so a field you omit is reset to its zero value. Send the object from GET /remotes/{id} with your changes applied.

public_key is one of those fields and no response returns it. An update that omits it clears the key, and the next belt remote start on that machine registers a new remote.

belt remote start overwrites status, system_info, remote_version, exec_enabled, and agents_enabled on its next registration, and name only when started with --name.


Update belt on a remote

POST /remotes/{id}/update

Asks the remote's daemon to update belt to the latest release in place, the way an engine updates. Requires remotes:write and manage on the remote (its owner, or a write share). No request body.

The daemon checks the release and answers within a few seconds. When a newer belt is available it then:

  1. refuses new commands, terminals, agent sessions and prompts (this remote is updating belt; try again in a minute)
  2. waits for running commands and agent turns to finish, up to 10 minutes
  3. installs the release while still connected (download, SHA-256 check, replace its own binary)
  4. restarts with the same arguments the way it was started (detached, as a service or in the foreground), with the same name and settings. On Linux and macOS it restarts in the same process; on Windows it starts a new one.

It reconnects on the new version, which registration records as remote_version. Idle agent sessions and open terminals end with the restart. If the install fails, the daemon takes work again on its current version without restarting, and remote_version stays as it was.

The daemon fetches the release itself from the CLI release manifest; the API never names a binary.

The response is the daemon's first answer. What happens next shows in the remote's last_update:

FieldTypeDescription
statestringSee below
targetstringEngines: what was asked for (stable, canary or a version). Empty for remotes, which always take the latest release.
fromstringVersion the machine ran when asked
tostringVersion it installs or was already on, when known
errorstringWhy, for refused and failed
atstringWhen state was reached
StateMeaning
requestedSent; the daemon hasn't answered. It stays here when the request times out (504 remote_timeout after 30 seconds).
installingA different release is being installed; the machine finishes its running work and restarts on it
installedThe machine reconnected on to
currentAlready on to; nothing installed
refusedThe binary can't replace itself: a development build, a package manager's install, or a directory the machine's user can't write to. error says what to do on the machine.
failedThe release couldn't be checked, downloaded or installed, or the request couldn't be sent to the daemon after the online check, so nothing will answer. error says why. The machine keeps its version.

An install that fails after the updating answer turns last_update to failed with the reason (belt v1.28.4 and later). The remote's page shows it under last update.

Response

FieldTypeDescription
request_idstringRequest id
statestringupdating (draining, then installs and restarts), current (already on the latest), unsupported (installed by a package manager, a development build, or a binary in a directory the daemon's user cannot write), or failed (the release could not be checked)
fromstringVersion the daemon runs
tostringLatest release, when known
errorstringWhy it cannot update, for unsupported and failed

Errors

HTTPCodeCause
409remote_offlineThe remote is not connected
504remote_timeoutThe daemon did not answer within 30 seconds (belt older than the release that added updates in place)

Set remote tags

PUT /remotes/{id}/tags

Replaces the remote's tags array in full. Requires remotes:write, read access to the remote, and the manage_policy capability on the remote's workspace (otherwise 403). Tags decide which workspace policy rules apply to the machine, so setting them is gated like editing policy rather than by ownership. An invalid tag returns 400 validation_error.

Request

FieldTypeDescription
tagsstring[]Complete tag list for the remote

Tags are normalized server-side: trimmed, lowercased, deduplicated, and sorted. Each tag must match ^[a-z0-9][a-z0-9_.-]{0,39}$ (lowercase letters, digits, -, _, or .). At most 20 tags per remote.

json
1{ "tags": ["prod", "eu-west"] }

Response

RemoteDTO with the updated tags field.

From the CLI, belt remote tag <id> [tag...] reads the current tags with GET /remotes/{id}, merges additions and --rm removals, then sends this request. See Remotes guide — Tags.


Delete remote

DELETE /remotes/{id}

Deletes a remote you own. Requires remotes:write. From the CLI, belt remote rm <id> sends the same request (confirmation prompt unless --yes).

bash
1belt remote rm remote_abc1232curl -X DELETE https://api.inference.sh/remotes/remote_abc123 \3  -H "Authorization: Bearer inf_your_key"

Deleting a row does not stop a daemon still running on the host. Stopping or reinstalling a machine often leaves an old registration; belt remote list hints at belt remote rm when a remote has not been seen for over a week.


Launch harness session

POST /remotes/{id}/sessions

Opens a new chat with a harness profile on a connected remote — for example Claude Code, Cursor, or Pi on your laptop, in a project folder. Requires remotes:write and agents:execute. The remote must have agents_enabled: true, the profile must belong to that remote, and the daemon must report capabilities for the harness (undriveable harnesses are not offered).

The first launch for a profile creates a harness agent (for example Claude Code · work-laptop) and reuses it on later launches. Connecting belt remote alone does not create agents anymore.

Request

FieldTypeRequiredDescription
profile_idstringYesProfile id from GET /remotes/{id} → profiles[].id
work_dirstringNoFolder the harness works in on the remote. Empty uses the daemon default.
resume_session_idstringNoContinue a harness session from List harness sessions instead of starting fresh

Response

Returns a ChatDTO (same shape as Get chat), including:

FieldDescription
nameTypically {display_name} · {remote name}
work_dirNormalized working directory for this chat
harness_session_idSet when resume_session_id was provided
agent_idThe harness agent created or reused for this profile

Send the first user message with POST /chats/:id/messages or POST /agents/run to start the harness turn.

Runtime on the remote

The chat exists in the API as soon as this endpoint returns; the harness process on the machine starts when the platform asks the daemon to open a session for that chat (typically on the first user message). Progress flows over the remote WebSocket (see Harness session frames):

Daemon → server eventMeaning
remote_session_openedHarness process is ready (harness_session_id, resumed, seq)
remote_session_eventSequenced harness output (AgentEvent payload, monotonic seq)
remote_session_closedSession ended; error when open or a turn failed

The daemon waits up to 60 seconds for the harness to finish opening. If the CLI is stuck on sign-in, an update prompt, or another interactive gate, it sends remote_session_closed with an error such as {display name} did not start a session within 1m0s. Check it works in a terminal on this machine: it may be waiting to sign in or to update. The API fails the active run with that reason. A harness that starts after the deadline is closed on the machine so the slot is freed.

When a user message cannot be delivered (no live session on the daemon), the daemon sends remote_session_closed so the run fails instead of staying in progress. When Prompt fails on a live session, the daemon emits an error agent event on the run before reporting failure.

Turn timeouts (belt with agentprotocol v0.12.1+). Opening a session is bounded by 60 seconds (above). Each user message is bounded only until the harness shows its first sign of work — an assistant message, tool call, permission request, plan update, or similar. Session bookkeeping and echoes of the user's prompt do not count. If nothing qualifies within 2 minutes, the daemon emits an error event with code no_response, cancels the in-flight prompt on the harness, and fails the run. After work has started, there is no fixed total prompt timeout; long tool loops can run until the harness completes the turn.

Harness stderr on failures. Session drivers keep the last 4 KiB of the harness process stderr. Open failures, no_response, process exit, and other driver errors append ; agent stderr: … to the error message on the error event and on remote_session_closed when applicable, so API clients and the workspace can surface CLI output without SSH access to the host.

Errors

HTTPCodeCause
400invalid_requestMissing or invalid body, or profile_id is empty
404profile_not_foundNo profile with that id on this remote
409agents_disabledThe daemon was not started with --agents
409harness_not_drivableThe profile has no reported capabilities
409harness_too_oldInstalled harness is below the driver's minimum (profiles[].support.level is older-than-supported). The error message includes what is missing and upgrade_cmd.
bash
1curl -X POST "https://api.inference.sh/remotes/remote_abc123/sessions" \2  -H "Authorization: Bearer inf_your_key" \3  -H "Content-Type: application/json" \4  -d '{"profile_id": "prof_xyz", "work_dir": "/home/you/project"}'

Heartbeat

POST /remotes/{id}/heartbeat

Records a liveness ping and updates heartbeat_at. Requires remotes:write. belt remote sends its heartbeats over the WebSocket. This endpoint is the HTTP equivalent. The body is optional. The response is a bare empty JSON object without the standard envelope:

json
1{}

revived is always false: heartbeats no longer change lifecycle status. Reconnection over the WebSocket moves a remote back to running.

To refresh system_info with live CPU, RAM, and home-disk usage, the daemon sends system_info on every third WebSocket remote_heartbeat frame instead of this HTTP route.


Dispatch exec

POST /remotes/{id}/execs

Runs a command on a connected remote. Returns an ExecRunDTO immediately. Output is read separately. The Exec Runs API page lists every response field.

Request

FieldTypeDescription
commandstringExecutable name or path (required)
argsstring[]Arguments
cwdstringWorking directory on the remote
envstring[]Extra KEY=VALUE entries
ptybooleanRun under a pseudo-terminal
timeout_msintegerTimeout in milliseconds (0 = no limit)
bash
1curl -X POST "https://api.inference.sh/remotes/remote_abc123/execs" \2  -H "Authorization: Bearer inf_your_key" \3  -H "Content-Type: application/json" \4  -d '{"command": "uname", "args": ["-a"]}'

Errors

HTTPCodeCause
400invalid_requestcommand is empty or the body is not valid JSON
403exec_disabledThe daemon was not started with belt remote start --exec
409remote_offlineThe remote is not connected

Get exec run

GET /execs/{id}

Returns an ExecRunDTO: command, status, exit code, timing, and last_seq (highest output sequence number).

statusTerminal?Meaning
pendingNoCreated, not yet sent to the host
runningNoSent to the host
exitedYesProcess ended. exit_code is set.
killedYesEnded without an exit code: signal, timeout, cancel, remote disconnect, or the daemon refused the run
deniedYesThe API could not deliver the command to the daemon

List exec runs

GET /execs or POST /execs/list

Cursor-paginated list of exec runs. Requires remotes:read.


Read exec output

GET /execs/{id}/output?after_seq={n}

Returns an array of ExecRunOutputDTO chunks with seq > after_seq, in order, up to 2000 per call. Poll with an increasing after_seq until the run reaches a terminal status.

FieldTypeDescription
seqintegerSequence number within the run
streamstringstdout or stderr
datastringOutput bytes, base64-encoded
bash
1curl "https://api.inference.sh/execs/exec_abc123/output?after_seq=0" \2  -H "Authorization: Bearer inf_your_key"

Signal exec

POST /execs/{id}/signal

Kills a running exec. Returns {"ok": true}. A run that is already terminal returns the same response and nothing is sent.

bash
1curl -X POST "https://api.inference.sh/execs/exec_abc123/signal" \2  -H "Authorization: Bearer inf_your_key"

WebSocket protocol

The daemon connects to /ws/remotes/{id} with Authorization: Bearer <api-key> and optional X-Team-ID. Requires remotes:write.

Server to remote:

EventPurpose
remote_exec_startStart a command (exec_run_id, command, args, cwd, env, pty, timeout_ms)
remote_exec_signalKill a run
remote_session_openStart or resume a harness session for a chat (chat_id, harness, cwd, optional resume_session_id, and tools when the profile has capabilities.tools: the platform catalog tool definitions)
remote_session_tool_resultAnswer a catalog tool call from the harness (chat_id, call_id, text, optional is_error). See Platform catalog tools
remote_session_promptSend a user message for a turn (chat_id, run_id, text, optional files: each {uri, filename, content_type, size}). Daemons older than belt v1.21.0 read only text
remote_session_interruptCancel the turn in flight, keep the session (chat_id)
remote_session_resolveAnswer a harness permission request (chat_id, request_id, decision, optional scope, reason). scope is once (one prompt) or session (the chat already allows that tool, or the person chose always allow; the harness may skip further prompts for that tool this session). See Remote harness runs.
remote_session_closeEnd the chat's session (chat_id)
remote_session_ackDrop buffered events stored through seq (chat_id, seq)
remote_session_replayResend buffered events after after_seq (chat_id, after_seq)
remote_sessions_listList harness sessions on disk (request_id, optional cwd filter)
remote_updateUpdate belt in place (request_id); see Update belt on a remote

Remote to server:

EventPurpose
remote_heartbeatLast-seen ping (updates heartbeat_at). Optional system_info on every third beat (~15s at default cadence) with live CPU, RAM, and home volume usage; stored on the remote when present. Optional harnesses with probed account and limits, right after connect and every 5 minutes, reconciled into profiles as at registration. Lifecycle status follows the socket, not each beat.
remote_exec_outputSequenced stdout/stderr chunk
remote_updatingAnswer to remote_update (request_id, state, from, to, error)
remote_exec_exitRun finished (exit_code, error, timed_out, duration_ms)
remote_session_openedSession is live (chat_id, harness_session_id, resumed, seq) — seq is the last event number the session has produced (0 for a process just launched; resets when the harness process restarts)
remote_session_eventOne harness event (chat_id, seq, event, optional gap on replay)
remote_session_closedSession ended (chat_id, optional error, for example when agents_enabled is false)
remote_session_tool_callThe harness called a platform catalog tool (chat_id, run_id, call_id, name, arguments, optional tool_invocation_id). The API runs it and answers with remote_session_tool_result
remote_sessions_listedAnswer to remote_sessions_list (request_id, sessions[], optional per-harness errors[])

Each entry in sessions includes harness, id, cwd, title, updated, and live (whether a process holds the session, with evidence and heuristic flags from the harness transcript layer). Listing reads metadata only, never transcript bodies.

Harness session frames require agents_enabled: true on the remote (set when the daemon starts with belt remote start --agents). The protocol also defines remote_terminal_* frames for interactive PTY sessions on the host. Opening, prompting, and closing harness sessions happens only over this socket; GET /remotes/{id}/sessions is the HTTP wrapper for listing metadata.

Ordering. The daemon handles incoming frames in parallel lanes keyed by chat_id, exec_run_id, or terminal session_id, so one slow harness session does not block exec output or another chat. Outbound frames use the same keys so session and exec events stay in order within each lane.

Open vs launch. remote_session_open reserves the chat's slot before the daemon replies with remote_session_opened; the harness process starts in the background. A remote_session_prompt sent immediately after open waits for that launch instead of racing an empty slot.

Persistence and replay. After each stored remote_session_event, the API sends remote_session_ack with the persisted sequence so the daemon can trim its replay buffer. On reconnect, the API sends remote_session_replay with after_seq set to the chat's stored position (harness_event_seq); events at or below that sequence are ignored. If the daemon restarts and reports a lower sequence in remote_session_opened, the stored position resets.

When a remote transitions to disconnected or stopped, unfinished exec runs and active harness agent runs on that remote fail (not retried). See Exec Runs API — Status and Remote harness runs.

→ Remotes guide — Agent harness sessions

Harness session frames

When agents_enabled is true, the API and daemon exchange harness chat control on the same WebSocket:

Server → remote

EventPurpose
remote_session_openStart or resume a harness for a chat (chat_id, harness kind, cwd, optional resume_session_id, tools for capabilities.tools profiles)
remote_session_tool_resultCatalog tool result for a prior remote_session_tool_call
remote_session_promptUser message for a turn (chat_id, run_id, text, attached files)
remote_session_interruptCancel the turn in flight
remote_session_resolveAnswer a permission prompt (request_id, decision, optional scope)
remote_session_closeEnd the chat's harness session
remote_session_ackDrop buffered events through seq
remote_session_replayResend events after after_seq
remote_sessions_listList harness sessions on the host (HTTP GET /remotes/{id}/sessions uses the same path)

Remote → server

EventPurpose
remote_session_openedOpen succeeded
remote_session_eventOne sequenced harness event
remote_session_closedSession ended (error when not a clean close)
remote_session_tool_callThe harness called a catalog tool; the API runs it and returns remote_session_tool_result
remote_sessions_listedAnswer to remote_sessions_list

Open is bounded by a 60 second deadline on the daemon. See Launch harness session — Runtime on the remote.


Scopes

ScopeAccess
remotes:readList and get remotes, list harness sessions, list and get exec runs, read output
remotes:writeRegister, update, and delete remotes, heartbeat, WebSocket connect, dispatch execs, signal runs, launch harness chats (also needs agents:execute), list, add and remove shares (GET /remotes/{id}/shares, POST /remotes/{id}/share, DELETE /remotes/{id}/share/{userId}, DELETE /remotes/{id}/team-share, DELETE /remotes/{id}/org-share)
agents:executeRequired together with remotes:write for POST /remotes/{id}/sessions

we use cookies

we use cookies to ensure you get the best experience on our website. for more information on how we use cookies, please see our cookie policy.

by clicking "accept", you agree to our use of cookies.
learn more.