Every command runs as a session. A session belongs to the sandbox, not to your connection: if the client goes away, the process keeps running and you can attach to it again.
Commands run as root with the environment and secrets of the sandbox. Exec wakes a sandbox that is not running.
Run a command over HTTP
POST /v1/sandboxes/{name}/exec| Query | About |
|---|---|
cmd |
One argument of the command. Repeat it for each one: cmd=ls&cmd=-la. Defaults to bash. |
path |
The program to run, in place of the first cmd. |
env |
KEY=VALUE, repeatable. Set over the environment of the sandbox. |
dir |
The working directory. |
stdin |
true to send the request body as stdin, up to 10 MiB. |
curl -X POST "$API/agent-1/exec" -H "$AUTH" --output - \
--url-query cmd=sh --url-query cmd=-c --url-query "cmd=echo hi; ls /nope" --url-query dir=/root--url-query (curl 7.87 and later) encodes each value for you.
The response is application/octet-stream and streams while the command runs. Each chunk starts with one byte that names its stream:
| Byte | Stream |
|---|---|
1 |
stdout |
2 |
stderr |
3 |
exit: one more byte, the exit code, and the end |
Pipe a file in as stdin:
curl -X POST "$API/agent-1/exec?cmd=wc&cmd=-l&stdin=true" -H "$AUTH" --data-binary @notes.txt --output -The HTTP form has no terminal. For an interactive shell, use the WebSocket.
Run a command over a WebSocket
GET /v1/sandboxes/{name}/exec (Upgrade: websocket)Takes the same query as the HTTP form, plus:
| Query | About |
|---|---|
tty |
true for a terminal. |
rows, cols |
The terminal size. |
max_run_after_disconnect |
How long the process may run once no client is attached: seconds, or a number with ms, s, m or h. Without it, the process runs until it ends. |
The first message is text:
{
"type": "session_info",
"session_id": 7,
"command": "bash",
"created": "2026-10-31T09:21:04Z",
"cols": 120,
"rows": 32,
"is_owner": true,
"tty": true,
"output_offset": 0
}Without a terminal, binary messages carry a stream byte and data, both ways:
| Byte | Direction | Stream |
|---|---|---|
0 |
to the sandbox | stdin |
4 |
to the sandbox | end of stdin |
1 |
from the sandbox | stdout |
2 |
from the sandbox | stderr |
3 |
from the sandbox | exit, with the exit code as one byte |
With tty=true, binary messages are raw terminal bytes in both directions, and the exit comes as text: {"type": "exit", "exit_code": 0}.
Text messages you can send:
{ "type": "resize", "cols": 160, "rows": 48 }
{ "type": "signal", "signal": "SIGTERM" }The socket carries SIGTERM only. To send another signal, kill the session.
Text messages you may receive, besides session_info and exit:
| Type | About |
|---|---|
error |
{ "type": "error", "message": "..." } |
gap |
{ "type": "gap", "bytes": 4096, "output_offset": 1052672 }. Output was dropped before you attached. |
behind |
{ "type": "behind", "output_offset": ... }. The client read too slowly; the socket closes with code 1013. Attach again from that offset. |
A minimal terminal client in Node, with the ws package:
import WebSocket from "ws";
const ws = new WebSocket("wss://api.nanite.run/v1/sandboxes/agent-1/exec?tty=true&cols=120&rows=32", {
headers: { Authorization: `Bearer ${process.env.NANITE_KEY}` },
});
ws.on("message", (data, binary) => {
if (binary) process.stdout.write(data);
else console.log(JSON.parse(data.toString()));
});
ws.on("open", () => ws.send(Buffer.from("uname -a\n")));Browsers cannot set headers on a WebSocket, so open sockets from your server.
List sessions
GET /v1/sandboxes/{name}/exec{
"sessions": [
{
"id": 7,
"command": "npm run build",
"workdir": "/root/app",
"tty": false,
"created": "2026-10-31T09:21:04Z",
"is_active": true,
"last_activity": "2026-10-31T09:22:40Z",
"bytes_per_second": 812
}
],
"count": 1
}A sandbox that is not running has no sessions.
Attach to a session
GET /v1/sandboxes/{name}/exec/{session_id} (Upgrade: websocket)Attaches to a session of a running sandbox and speaks the same messages as a new one. Each session keeps the last 1 MiB of its output; pass output_offset, a byte offset from session_info or from your own count, to replay from where you left off. If part of what you asked for is gone, a gap message says how much. An unknown session answers 404, and so does any session of a sandbox that is not running.
Kill a session
POST /v1/sandboxes/{name}/exec/{session_id}/kill| Query | About |
|---|---|
signal |
The signal to send first. Defaults to SIGTERM. |
timeout |
Seconds to wait before SIGKILL. Defaults to 10. |
The answer is NDJSON, one event per line, ending with complete or error:
{"type":"signal","message":"Signaling SIGTERM to the processes of session 7","signal":"SIGTERM","pid":412}
{"type":"exited","message":"Process exited"}
{"type":"complete","exit_code":143}Other events are timeout (the process outlived the timeout) and killed (it was sent SIGKILL).