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).
Concepts
| Resource | Description |
|---|---|
| Remote | A machine running belt remote. It hosts agent harness profiles and optional shell execution. |
| Profile | One harness slot on a remote (for example Claude Code). Profiles are created from daemon registration. There is no REST endpoint that creates them. |
| Exec run | One 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). 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 --exec. The API refuses dispatch when it is false, and the daemon refuses the command frame as well.
Register remote
POST /remotes
Called by belt remote on startup. Registration keys on the daemon's public_key for your user. A machine that registers again with the same key reuses its existing remote and does not create a duplicate. Each registration also reconciles the harnesses probe results into profiles (one per harness kind) and returns the remote with profiles populated.
Request
| Field | Type | Description |
|---|---|---|
name | string | Display name. belt remote sends the hostname unless --name is set. |
public_key | string | Base64 ed25519 public key that identifies the machine |
remote_version | string | belt version string |
system_info | object | Host metadata (see below) |
exec_enabled | boolean | Whether this daemon accepts command execution (belt remote --exec) |
harnesses | array | Harnesses discovered on the host (see below) |
The handler does not validate any of these fields as required.
system_info
belt remote sends this subset of the API's SystemInfo object. Fields it does not report (gpus, docker, volumes, and others) default to zero values.
| Field | Type | Description |
|---|---|---|
hostname | string | Machine hostname |
os | string | OS label, for example Ubuntu 24.04 LTS or macOS |
cpus | array | CPU entries with name, cores, and frequency |
ram | object | total is physical memory in bytes |
1{2 "hostname": "work-laptop",3 "os": "macOS",4 "cpus": [{ "name": "Apple M3 Pro", "cores": 12 }],5 "ram": { "total": 38654705664 }6}harnesses entries
Each object describes one agent harness binary found on PATH:
| Field | Type | Description |
|---|---|---|
kind | string | Harness identifier: claude-code, codex, gemini, cursor, opencode, or amp |
command | string | Resolved path to the harness binary |
version | string | First line of <command> --version. Optional, best effort. |
logged_in | boolean | true when a vendor auth file exists on the host. The daemon checks existence only and never reads the file. |
A harness kind that is missing from the array is not installed, or not on PATH.
Profile reconciliation
On each registration the API reconciles harnesses into the remote's profiles:
| Registration input | Profile effect |
|---|---|
New kind | Creates a profile with harness_kind, command, and status |
Existing kind | Updates command and status in place (same profile id) |
logged_in: true | status becomes idle |
logged_in: false | status becomes unavailable |
| Harness no longer reported | The profile is marked unavailable. It is not deleted. |
The API never receives harness credentials.
Response
RemoteDTO:
| Field | Type | Description |
|---|---|---|
id | string | Remote ID |
name | string | Display name |
status | string | Lifecycle status (see below) |
heartbeat_at | string | null | Time of the last heartbeat |
system_info | object | null | Host metadata from registration |
remote_version | string | belt version the daemon reported |
exec_enabled | boolean | Whether the host opted in to command execution |
profiles | array | Harness profiles (see below) |
public_key is not part of the response.
Profile
Each ProfileDTO in profiles:
| Field | Type | Description |
|---|---|---|
remote_id | string | Owning remote |
harness_kind | string | Harness type (claude-code, codex, ...) |
name | string | Display name for this slot |
command | string | Harness binary path on the remote |
args | string[] | Extra launch arguments |
status | string | idle, busy, or unavailable |
max_concurrent | integer | Concurrency cap for this profile |
Profile status | Meaning |
|---|---|
idle | Ready to take a run |
busy | At its concurrency limit |
unavailable | Logged out, removed from the host, rate-limited, or disabled |
Remote lifecycle status:
status | Meaning |
|---|---|
pending | Registered, not yet connected |
running | Connected and sending heartbeats |
disconnected | Heartbeat is stale. Reversible on reconnect. |
stopped | Finalized after prolonged disconnection |
List remotes
GET /remotes or POST /remotes/list
Cursor-paginated list of remotes. Requires remotes:read.
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 prints when it connects, or one from the list endpoint.
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 overwrites name, status, system_info, remote_version, and exec_enabled on its next registration.
Delete remote
DELETE /remotes/{id}
Deletes a remote you own. Requires remotes:write.
1curl -X DELETE https://api.inference.sh/remotes/remote_abc123 \2 -H "Authorization: Bearer inf_your_key"Heartbeat
POST /remotes/{id}/heartbeat
Records a liveness ping. 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 JSON object without the standard envelope:
1{ "revived": false }revived is true when the remote was disconnected and this beat moved it back to running.
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
| Field | Type | Description |
|---|---|---|
command | string | Executable name or path (required) |
args | string[] | Arguments |
cwd | string | Working directory on the remote |
env | string[] | Extra KEY=VALUE entries |
pty | boolean | Run under a pseudo-terminal |
timeout_ms | integer | Timeout in milliseconds (0 = no limit) |
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
| HTTP | Code | Cause |
|---|---|---|
400 | invalid_request | command is empty or the body is not valid JSON |
403 | exec_disabled | The daemon was not started with --exec |
409 | remote_offline | The 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).
status | Terminal? | Meaning |
|---|---|---|
pending | No | Created, not yet sent to the host |
running | No | Sent to the host |
exited | Yes | Process ended. exit_code is set. |
killed | Yes | Ended without an exit code: signal, timeout, cancel, or the daemon refused the run |
denied | Yes | The 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.
| Field | Type | Description |
|---|---|---|
seq | integer | Sequence number within the run |
stream | string | stdout or stderr |
data | string | Output bytes, base64-encoded |
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.
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:
| Event | Purpose |
|---|---|
remote_exec_start | Start a command (exec_run_id, command, args, cwd, env, pty, timeout_ms) |
remote_exec_signal | Kill a run |
Remote to server:
| Event | Purpose |
|---|---|
remote_heartbeat | Liveness (status: running) |
remote_exec_output | Sequenced stdout/stderr chunk |
remote_exec_exit | Run finished (exit_code, error, timed_out, duration_ms) |
The protocol also defines remote_terminal_* frames for PTY sessions. No REST endpoint opens a terminal session, and the API only logs the terminal frames it receives.
Scopes
| Scope | Access |
|---|---|
remotes:read | List and get remotes, list and get exec runs, read output |
remotes:write | Register, update, and delete remotes, heartbeat, WebSocket connect, dispatch execs, signal runs |