---
title: "Run a command"
method: POST
path: "/sandboxes/{id}/exec"
tags: ["Execution"]
---

# Run a command

`POST /sandboxes/{id}/exec`

## Request

- Base URL: `https://sandbox-as-a-service.com/v1`
- URL: `https://sandbox-as-a-service.com/v1/sandboxes/{id}/exec`
- Auth: HTTP bearer

Runs a shell command inside the sandbox as an unprivileged user and waits for it to finish. Output is capped at 1 MiB per stream; anything beyond that sets `truncated`.

With `stream: true` the response is a Server-Sent Events stream instead of a single JSON body: output arrives as it is produced, and disconnecting before the terminal event kills the command (recorded with `status: "cancelled"`). The terminal `exit` event carries the same object the non-streaming call returns.

The call returns when nothing it started still holds the output stream open — not when the foreground command exits, so a trailing `&` alone does not end it. In particular `cd /workspace && nohup server &` backgrounds a *subshell*, which keeps the call open for the full `timeout_ms` and then returns `exit_code: 124`; when the timeout fires, that command and every process it started are killed, including the detached one. Pass `cwd` instead of prefixing `cd`.

## Request body

- object
  - `command` string, required
  - `timeout_ms` integer
  - `cwd` string: Working directory. Prefer this over a `cd ... &&` prefix, which breaks backgrounding with `&`.
  - `env` object: Environment variables for this command.
  - `stream` boolean: When true, the response is a Server-Sent Events stream (see the `text/event-stream` variant of the 200 response) and output arrives as it is produced. Absent or false gives the plain JSON result.

## Response `200`

The command finished. A non-zero `exit_code` is still a 200 response. With `stream: true` the body is a `text/event-stream` (headers `Cache-Control: no-cache, no-transform` and `X-Accel-Buffering: no`) instead of a single JSON document. Its events: `start` once, first, as `{"id":"exec_…","sandbox_id":"…"}`; `stdout`/`stderr` as `{"data":"<chunk>"}` as output arrives (a multi-byte UTF-8 character split across transport chunks is delivered whole); the comment line `: ping` at least every 15 s as a heartbeat; and exactly one terminal `exit` whose data is the same Execution object the non-streaming call returns (a failure after the stream started is instead one `error` event carrying the standard error object). Disconnecting before `exit` kills the command and records it with `status: "cancelled"`. Output past the 1 MiB per-stream storage cap keeps streaming live but is not stored; the stored record and the `exit` event then carry `truncated: true`.

- Execution
  - `object` 'execution'
  - `id` string
  - `sandbox_id` string
  - `status` 'running' | 'completed' | 'timeout' | 'failed' | 'cancelled': `cancelled` means the client disconnected from a streamed execution; the command was killed.
  - `exit_code` integer, nullable
  - `stdout` string
  - `stderr` string
  - `truncated` boolean: True when output exceeded 1 MiB per stream and was cut off.
  - `duration_ms` integer, nullable
  - `started_at` string, date-time, nullable
  - `finished_at` string, date-time, nullable

## Other responses

- `400`: Invalid request parameters.
- `401`: Missing or invalid API key.
- `402`: Credit balance exhausted.
- `404`: No sandbox with that id for this account.
- `409`: Sandbox is not running.

---

[API](https://skmtc.dev/sandbox-as-a-service/apis/sandbox-as-a-service-api.md) · [All operations](https://skmtc.dev/sandbox-as-a-service/apis/sandbox-as-a-service-api/llms.txt) · [OpenAPI document](https://skmtc.dev/sandbox-as-a-service/apis/sandbox-as-a-service-api/revisions/4f1ea2cf8333?raw)
