Cloud API Reference

Surface: hosted cloud API. This documents the hosted smolmachines cloud REST API at api.smolmachines.com. To drive machines (local or cloud) from code with the smolmachines SDK’s in-process engine — no server — see the Machine API reference and the guides.

Base URL: https://api.smolmachines.com

All endpoints require Authorization: Bearer <api_key> header.

Request IDs

Every response — success or error — carries an x-request-id header uniquely identifying that request. When something goes wrong, grab it and include it in your support message: it lets us pull the exact server-side logs for your call immediately, with no need to send timestamps or machine names.

curl -sD - -o /dev/null https://api.smolmachines.com/v1/machines \
  -H "Authorization: Bearer $SMOLCLOUD_KEY" | grep -i x-request-id
# x-request-id: dc69cd24-a0b3-4a71-b842-719570632974

To stitch your own tracing across the call, send your own x-request-id on the request and it’s echoed back on the response (a missing or unsafe value is replaced with a fresh server-generated ID). Read it from your HTTP client’s response headers on every call, including the failing one.

Machines

Create a machine

POST /v1/machines
{
  "name": "my-machine",
  "source": { "type": "image", "reference": "alpine" },
  "resources": { "cpus": 1, "memoryMb": 256, "diskGb": 20 },
  "network": { "mode": "open" },
  "env": { "APP_ENV": "production" },
  "workdir": "/workspace",
  "autoStopSeconds": 300,
  "ttlSeconds": 3600,
  "ephemeral": false,
  "mounts": [{ "volume": "data-vol", "mountPath": "/data" }],
  "ports": [{ "port": 8080 }],
  "public": false
}

All fields except source are optional (name included — omit it for an unnamed machine). Returns 201 Created with the machine object in state: "stopped".

Source. Two source shapes are accepted:

  • { "type": "image", "reference": "alpine" } — an OCI image reference.
  • { "type": "smolmachine", "reference": "myapp:v1", "arch": "arm64" } — a packed .smolmachine registry artifact. arch (arm64/amd64) is optional; when omitted the control plane resolves it from the artifact manifest and pins placement to a matching node.

Resources. resources accepts only cpus, memoryMb, and diskGb. Defaults are 1 vCPU / 256 MB when omitted; diskGb defaults to the image’s own size and may not exceed 16 TiB (16384).

Network. network.mode defaults to blocked — omitting network means the machine has no outbound egress. Modes:

  • { "mode": "blocked" } — no egress (default).
  • { "mode": "open" } — unrestricted egress.
  • { "mode": "allowCidrs", "cidrs": ["10.0.0.0/8"], "hosts": ["api.example.com"] } — allow only the listed CIDRs/hosts.

Ports & public ingress. Supply ports: [{ "port": 8080 }] with guest ports only (max 4); the control plane allocates a hostPort at first start. Set public: true to expose a published port on the unauthenticated public ingress; the default (false) keeps it reachable only via the authenticated path.

Response (abbreviated):

{
  "id": "mach-abc123",
  "name": "my-machine",
  "source": { "type": "image", "reference": "alpine" },
  "arch": "arm64",
  "state": "stopped",
  "resources": { "cpus": 1, "memoryMb": 256, "diskGb": 20 },
  "network": { "mode": "open" },
  "env": { "APP_ENV": "production" },
  "workdir": "/workspace",
  "autoStopSeconds": 300,
  "ttlSeconds": 3600,
  "ephemeral": false,
  "ports": [{ "port": 8080, "hostPort": 34071 }],
  "url": null,
  "createdAt": "2026-07-01T12:00:00Z",
  "updatedAt": "2026-07-01T12:00:00Z",
  "lastActivityAt": null,
  "ready": false,
  "readyAt": null
}

arch is the machine’s actual CPU arch. url is the public ingress URL for the first published port — null when the machine is stopped, has no published port, or the deployment exposes no public base URL. hostPort is populated once the machine is placed.

Readiness — wait for ready, not state. state: "started" means only that the VM process launched; the guest OS and your in-VM workload still need a moment to boot. ready becomes true once the guest agent is reachable (an exec/connect will succeed) and, if the machine publishes a port, that port is accepting connections; readyAt records when. Acting on started alone is the most common integration bug: on a warm start a machine reaches started in under a second, so a client that immediately expects its workload to be up will time out and tear the machine down before it finished booting (on a cold start the image pull hides this, so it “works the first time” and then fails intermittently). Poll GET /v1/machines/:id until ready is true — with a generous ceiling (≥20–30 s) and ~0.5–1 s between polls — before driving the machine or deleting it. Older machines that predate this field simply omit it; fall back to state there.

List machines

GET /v1/machines

Returns an array of all your machines.

Get a machine

GET /v1/machines/:id

Start a machine

POST /v1/machines/:id/start

Stop a machine

POST /v1/machines/:id/stop

Compute billing stops immediately. Storage persists.

Delete a machine

DELETE /v1/machines/:id

Other machine endpoints

MethodPathPurpose
POST/v1/machines/:id/forkFork a running, forkable golden into a new copy-on-write clone. Returns 201.
POST/v1/machines/:id/shareMint (or rotate) an anonymous share token + URL for the machine’s public app ingress.
DELETE/v1/machines/:id/shareRevoke the share token (idempotent, 204).
GET/v1/machines/:id/logsStream the machine’s console logs. ?follow=true follows; ?tail=N caps the backlog.
POST/v1/machines/:id/snapshotCapture the machine’s overlay to object storage. Returns 201 (503 if snapshots are unavailable on the deployment).
GET/v1/machines/:id/snapshotsList captured snapshots.
GET/v1/machines/:id/imagesList OCI images cached inside the machine.
POST/v1/machines/:id/imagesPull an image into the machine ({ "image": "...", "platform": "linux/arm64" }).
GET/v1/machines/:id/usagePer-machine usage + cost for the current billing period.

Connect to a machine’s port

GET /v1/machines/:id/connect/:port
GET /v1/machines/:id/connect/:port/*rest

Bridge an authenticated connection straight to a published port inside the guest — no public exposure, no tunnel, no egress allow-list. Reaches a service the guest is listening on (:port is the guest port you published at create). The request is authenticated with your normal Authorization: Bearer <api_key> and scoped to your tenant.

Both transports are supported on the same route:

  • HTTP — a plain request is proxied to http://<guest>:<port>/<rest> and the response streamed back.
  • WebSocket — send an Upgrade: websocket handshake and frames are relayed bidirectionally to the guest port (subprotocols preserved).

The machine must be started and must publish :port (ports: [{ "port": <port> }] at create). Responses: 404 if the machine isn’t yours or the port isn’t published, 503 if the machine isn’t started or placed, 403 if the key’s scope is denied.

This inverts the fragile “guest dials out to a relay” pattern into a robust “you dial in”, and doubles as a readiness probe — a successful connect means the workload is up. A quick health check:

curl "$SMOLCLOUD_URL/v1/machines/MACHINE_ID/connect/8080/healthz" \
  -H "Authorization: Bearer $SMOLCLOUD_KEY"

Exec

Execute a command

POST /v1/machines/:id/exec
{
  "command": "echo hello",
  "env": { "MY_VAR": "value" },
  "cwd": "/workspace",
  "timeoutSeconds": 30,
  "stdin": "input data\n",
  "stream": false,
  "background": false
}

command can be a string (runs via sh -lc) or an array (argv form: ["python3", "app.py"]).

Set background: true to spawn the command detached and return immediately with its PID — use it for long-lived daemons (a dev server, an agent runner).

If the machine is stopped, it auto-starts before executing.

TIP

exec calls are never charged — they are metered for the event timeline but priced at $0. They are still subject to the standard per-tenant rate limit, so a burst can return 429.

Response:

{
  "stdout": "hello\n",
  "stderr": "",
  "exitCode": 0,
  "durationMs": 14,
  "machineId": "mach-abc123",
  "stdoutTruncated": false,
  "stderrTruncated": false
}

stdoutTruncated/stderrTruncated are true when the captured stream exceeded the server’s per-call size cap.

Streaming exec (SSE)

Set "stream": true to receive Server-Sent Events as the command runs.

Execute code

POST /v1/machines/:id/code
{
  "language": "python",
  "code": "print(sum(range(100)))"
}

Supported languages: python (aliases python3), javascript (aliases js, node).

Files

Upload a file

PUT /v1/machines/:id/files/{path}

Body: raw file bytes.

Download a file

GET /v1/machines/:id/files/{path}

Sessions

Sessions maintain environment variables and working directory across multiple exec calls.

Create a session

POST /v1/machines/:id/sessions
{
  "cwd": "/workspace",
  "env": { "PROJECT": "myapp" }
}

Execute in a session

POST /v1/machines/:id/sessions/:sessionId/exec

Same body as regular exec. Session env and cwd are inherited (request overrides).

List sessions

GET /v1/machines/:id/sessions

Delete a session

DELETE /v1/machines/:id/sessions/:sessionId

Events

Get machine events

GET /v1/machines/:id/events

Returns lifecycle events (start failures, rescheduling, etc.) for debugging.

Volumes

Create a volume

POST /v1/volumes
{
  "name": "my-data",
  "sizeGb": 10
}

List volumes

GET /v1/volumes

Get a volume

GET /v1/volumes/:id

Delete a volume

DELETE /v1/volumes/:id

Mount volumes when creating a machine using the mounts field.

Usage

Query usage

GET /v1/usage?tenantId=TENANT&from=ISO_DATE&to=ISO_DATE

Response:

{
  "tenantId": "acme-corp",
  "from": "2026-06-01T00:00:00Z",
  "to": "2026-07-01T00:00:00Z",
  "totalUptimeSeconds": 86400,
  "cpuHours": 48.0,
  "memoryGbHours": 12.0,
  "diskGbHours": 480.0,
  "egressGb": 3.2,
  "machineCount": 5
}

from/to echo the queried window. Exec is intentionally absent — it is metered but priced at $0.

API Keys

Create an API key

POST /v1/apikeys
{
  "description": "CI deploy key",
  "scopes": ["machine:create", "machine:exec"],
  "expiresInDays": 30
}

List API keys

GET /v1/apikeys

Revoke an API key

DELETE /v1/apikeys/:id

Health

GET /health

Public endpoint, no auth required.

Error responses

All errors return a plain text body with an appropriate HTTP status code:

StatusMeaning
400Bad request (validation failed)
401Unauthorized (missing or invalid key)
402Payment required (tenant suspended or over budget)
403Forbidden (insufficient scopes)
404Not found (or belongs to another tenant)
409Conflict (duplicate name)
422Unprocessable (quota exceeded, concurrency limit, no capacity)
429Rate limited
500Internal server error
503Service unavailable (e.g. snapshots not configured on the deployment)