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 thesmolmachinesSDK’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.smolmachineregistry 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
| Method | Path | Purpose |
|---|---|---|
POST | /v1/machines/:id/fork | Fork a running, forkable golden into a new copy-on-write clone. Returns 201. |
POST | /v1/machines/:id/share | Mint (or rotate) an anonymous share token + URL for the machine’s public app ingress. |
DELETE | /v1/machines/:id/share | Revoke the share token (idempotent, 204). |
GET | /v1/machines/:id/logs | Stream the machine’s console logs. ?follow=true follows; ?tail=N caps the backlog. |
POST | /v1/machines/:id/snapshot | Capture the machine’s overlay to object storage. Returns 201 (503 if snapshots are unavailable on the deployment). |
GET | /v1/machines/:id/snapshots | List captured snapshots. |
GET | /v1/machines/:id/images | List OCI images cached inside the machine. |
POST | /v1/machines/:id/images | Pull an image into the machine ({ "image": "...", "platform": "linux/arm64" }). |
GET | /v1/machines/:id/usage | Per-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: websockethandshake 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:
| Status | Meaning |
|---|---|
| 400 | Bad request (validation failed) |
| 401 | Unauthorized (missing or invalid key) |
| 402 | Payment required (tenant suspended or over budget) |
| 403 | Forbidden (insufficient scopes) |
| 404 | Not found (or belongs to another tenant) |
| 409 | Conflict (duplicate name) |
| 422 | Unprocessable (quota exceeded, concurrency limit, no capacity) |
| 429 | Rate limited |
| 500 | Internal server error |
| 503 | Service unavailable (e.g. snapshots not configured on the deployment) |