Cloud API Reference
The Cloud API is the hosted REST interface for smol cloud.
- Base URL:
https://api.smolmachines.com - Generated OpenAPI: smolmachines.com/openapi.json
- Interactive client: API Explorer
Use the generated OpenAPI document for the routes and schemas it includes. It is generated from the service, so it still lists volume routes; those are not a supported part of the API and a machine created with a mounts field comes up without the storage. Specialized operations and newly added fields can land ahead of the website’s generated snapshot, so use this page and the linked lifecycle guides for behavior that the schema does not yet describe.
Authentication
Create an API key in the console and send it as a bearer token:
Authorization: Bearer smk_your_key_here For shell examples:
export SMOL_CLOUD_TOKEN="smk_your_key_here"
export SMOL_CLOUD_URL="https://api.smolmachines.com" Keep API keys on trusted servers. Use separate keys for development, CI, and production, and revoke keys that are no longer needed.
Core resources
Machines
Machines are persistent cloud microVMs. The core lifecycle is:
POST /v1/machines
GET /v1/machines
GET /v1/machines/{id}
POST /v1/machines/{id}/start
POST /v1/machines/{id}/stop
POST /v1/machines/{id}/pause
POST /v1/machines/{id}/resume
DELETE /v1/machines/{id} pause saves the machine’s memory and disks durably, then stops it; resume starts it again from that point, under the same id. Both need machine:create, and each answers 409 when the machine is in no state to do it: not pausable, or no saved execution ready.
Machine creation accepts an OCI image or a .smolmachine registry reference as its source. Specify CPU, memory, network policy, environment, working directory, lifecycle limits, and other supported fields in the create request.
When resources is omitted, a machine currently defaults to 4 vCPUs and 8192 MB of memory. When network is omitted, outbound access defaults to open — set network.mode explicitly (blocked or allowCidrs) when egress policy matters. Note a blocked machine also cannot pull its image, which runs in-guest, unless the image is already cached on its node. Larger machines cost more, so pass resources explicitly (for example {"cpus": 1, "memoryMb": 256}) rather than relying on the default. Disks can be configured up to 16 TiB where capacity is available. Check the OpenAPI document for the current request limits.
resources.diskDurability is full (the default), where a guest fsync waits for the node’s disk, or deferred, which is faster for fsync-heavy work and can lose recent writes if the node crashes; a branch keeps its source’s.
Machine and session responses list env with every value replaced by [REDACTED]. Values reach the machine but are never read back, so do not copy a response’s env into a new create.
nestedVirt: true exposes the host’s virtualization extensions so the guest can run its own hypervisor. It is included with the Startup and Enterprise plans; on any other plan the create is refused with 422 nested_virt_unavailable, naming the plan. It is also refused with the same code when no node offers it.
Commands and sessions
Run a command with:
POST /v1/machines/{id}/exec command accepts a shell string or an argv array. Requests can also include cwd, env, stdin, timeoutSeconds, stream, and background for detached execution. Verify the generated schema for the deployment you target before relying on optional fields.
The response carries each output stream twice: as UTF-8 text (stdout/stderr, capped at 1 MiB) and as byte-exact base64 (stdoutB64/stderrB64). Pass ?output=text or ?output=b64 to receive only one family and halve the response size; the default returns both.
Sessions preserve a working directory and environment across related exec calls:
GET /v1/machines/{id}/sessions
POST /v1/machines/{id}/sessions
POST /v1/machines/{id}/sessions/{sessionId}/exec
DELETE /v1/machines/{id}/sessions/{sessionId} A job runs a command detached and keeps its stdout, stderr and exit code in the machine, so a client can disconnect and come back for them:
POST /v1/machines/{id}/jobs
GET /v1/machines/{id}/jobs
GET /v1/machines/{id}/jobs/{job_id}
GET /v1/machines/{id}/jobs/{job_id}/output
POST /v1/machines/{id}/jobs/{job_id}/cancel POST takes command, with optional cwd, env, user and timeoutSeconds, and returns 202 at once; it wakes a stopped machine, and a running job keeps the machine from auto-stopping. A job’s state is running, exited with its exitCode, or lost when the machine stopped or restarted
while it ran. Read output with ?stream=stdout or stderr from an offset, asking again from nextOffset until eof. Cancel sends SIGTERM, and a cancelled or timed-out job records exit
code 143. The image needs sh.
Interactive terminal
The console’s terminal is a WebSocket you can open yourself, to give your own product a real shell in a machine: full-screen programs, colors, prompts, and resizing all work.
GET /v1/machines/{id}/exec/interactive (WebSocket upgrade) | Query parameter | Meaning |
|---|---|
cmd | Program to run, as a single path with no arguments. Defaults to /bin/sh. command is accepted too. |
cols, rows | Initial terminal size. |
access_token | Your API key, for clients that cannot set an Authorization header on a WebSocket, such as browsers. Servers should send the bearer header instead. |
The key needs the machine:exec scope. A stopped machine is started before the terminal opens.
Once connected, frames mean:
| Direction | Frame | Meaning |
|---|---|---|
| you → machine | binary | Keystrokes, as raw bytes |
| you → machine | text {"type":"resize","cols":120,"rows":40} | The terminal was resized |
| you → machine | text {"type":"stdin","data":"ls\n"} | Text input; any other text frame is also typed as input |
| machine → you | binary | Terminal output, including ANSI escape codes |
| machine → you | text {"type":"exit","code":0} | The program exited; the socket closes next |
This maps directly onto xterm.js:
import { Terminal } from '@xterm/xterm';
const term = new Terminal();
term.open(document.getElementById('terminal'));
const url = new URL(`wss://api.smolmachines.com/v1/machines/${machineId}/exec/interactive`);
url.search = new URLSearchParams({
cmd: '/bin/bash',
cols: String(term.cols),
rows: String(term.rows),
access_token: apiKey,
}).toString();
const ws = new WebSocket(url);
ws.binaryType = 'arraybuffer';
const encoder = new TextEncoder();
ws.onmessage = (event) => {
if (typeof event.data === 'string') {
const msg = JSON.parse(event.data);
if (msg.type === 'exit') term.write(`\r\n[exited with ${msg.code}]\r\n`);
} else {
term.write(new Uint8Array(event.data));
}
};
term.onData((data) => ws.send(encoder.encode(data)));
term.onResize(({ cols, rows }) => ws.send(JSON.stringify({ type: 'resize', cols, rows }))); From a server, the same session with Node’s ws package:
import WebSocket from 'ws';
const ws = new WebSocket(
`wss://api.smolmachines.com/v1/machines/${machineId}/exec/interactive?cmd=/bin/sh&cols=120&rows=40`,
{ headers: { Authorization: `Bearer ${process.env.SMOL_CLOUD_TOKEN}` } },
);
ws.on('open', () => ws.send(Buffer.from('uname -a\n')));
ws.on('message', (data, isBinary) => {
if (isBinary) process.stdout.write(data);
else if (JSON.parse(data.toString()).type === 'exit') ws.close();
}); Behavior to plan for:
- Closing the socket ends the program. Nothing keeps running once you disconnect. For a session you can come back to, set
cmdto a script that runsexec tmux new -A -s main(withtmuxinstalled in the machine) and reconnect to the same session. - A connection lasts at most one hour. Reconnect before then; with
tmux, reconnecting lands you back where you were. - Quiet sessions are kept alive. The server pings every 30 seconds, which WebSocket clients answer automatically. A client that stops answering for 90 seconds is treated as gone and its session ends, so a machine is not kept awake by a connection nobody is using.
- Several terminals can be open at once. Each has its own session and does not block the others or
execcalls. - Treat a key in a browser as visible. Anyone who can read the page can read an
access_token, and query strings can appear in proxy logs. Give the page a key of its own, created in the console with only themachine:execscope and a short expiry, and revoke it when the session ends.
For command output without a terminal, such as an agent running a build, POST /v1/machines/{id}/exec with "stream": true returns output as Server-Sent Events and is simpler to consume.
Files
The API supports file upload and download for a machine. Use the current OpenAPI or API Explorer for the path shape and request encoding.
Request bodies are capped: 1 MiB on the JSON API, 100 MiB on a file upload to /v1/machines/{id}/files/..., and 10 MiB per request through the connect route. A larger body is refused.
Upload targets follow the machine’s filesystem layout. Write to /workspace or another path on the storage disk when the file must survive a stop and start; /tmp is empty after the machine restarts. An upload whose target resolves through a symlink into a memory-backed path fails instead of writing the file. See Cloud Lifecycle, Storage, and Networking.
Operational endpoints
The Cloud API also provides endpoints for:
- Running Python or JavaScript through
/code - Machine events and logs
- Per-machine usage and cached images
- Sharing a machine through scoped share links
- Branching a branchable machine
- Exporting a machine to a
.smolmachineartifact - Resizing a running machine’s vCPUs, memory or disk in place,
POST /v1/machines/{id}/resize - Minting a short-lived token that opens only one machine’s connect bridge,
POST /v1/machines/{id}/connect-token - Replacing a machine’s published ports, live while it runs,
PUT /v1/machines/{id}/ports - Getting or revoking login-free port links, one URL per published port,
POSTandDELETE /v1/machines/{id}/port-links - Listing every checkpoint the account owns,
GET /v1/checkpoints, and uploading a.smolcheckpointfile captured outside the cloud withPOST /v1/checkpointsand thenPOST /v1/checkpoints/{id}/complete - Rolling a machine back in place to its newest or a named checkpoint, keeping its id and name,
POST /v1/machines/{id}/rollback
Use the OpenAPI document or Explorer to inspect the exact routes, request bodies, and feature availability for your account.
Usage and account
Use /v1/usage for usage over a time range and GET /v1/machines/{id}/usage for one machine’s totals and cost. The API also exposes account and billing-meter endpoints where enabled. Plan limits and pricing can change; use the pricing page for current public rates.
TIP
exec calls are never charged. They are metered for the event timeline but
priced at $0, so they do not appear among the priced usage dimensions. They are
still subject to the per-tenant rate limit, so a burst can return 429.
Metering semantics to build billing on:
- Uptime, base, and disk accrue in near real time. CPU and memory accrue through a metering rollup that samples every few minutes, so a mid-life
/usageread is a lower bound on the eventual cost — not the settled number. - Stop and delete take a synchronous final metering sample, so usage is fully settled the moment a machine ends.
DELETE /v1/machines/{id}?includeUsage=truereturns200with the settled usage and cost in the response body — the recommended pattern for short-lived machines (create, run a job, delete, bill from the DELETE response). Without the flag, DELETE returns204as before.GET /v1/machines/{id}/usagekeeps working for 30 days after a machine is deleted.
When the prepaid balance runs out, running work continues for 48 hours, with an email at the start and before the end. Then running machines are stopped, and requests that start or create work are refused with 402 (payment_required, detail “balance exhausted”). Reads, stop and delete still work. A plan with a monthly budget, or the egress budget, refuses the same way when reached. PATCH /v1/account with lowBalanceThresholdMicros sets the low-balance email (0 turns it off), and auto-recharge, set from billing, charges the card on file a chosen amount when the balance falls to a threshold.
The cloud-usage skill packet reads these totals and reconciles one machine’s bill against its uptime and the published rate.
API keys
Keys are listed, created and revoked through these routes:
GET /v1/apikeys
POST /v1/apikeys
DELETE /v1/apikeys/{id} Each needs the members:write scope, which a signed-in owner or admin holds and a newly created key
cannot carry: a key may hold only machine:, volume:, usage:read and group:write scopes. So
keys are managed from the console, and a call with an API key gets a 403 naming the
missing scope. The plaintext value of a newly created key is shown once.
Shared accounts, their members and invitations, and sub-organizations have routes under /v1/org, described on Organizations.
Readiness and lifecycle
Do not treat state: "started" as application readiness. Poll GET /v1/machines/{id} until ready is true before executing dependent work or connecting to a published service. ready flips once the machine answers a probe — its published port accepts connections, or, for machines with no published port, its guest agent responds — normally within a few seconds of start; readyAt records when. Exec does not require readiness: it auto-starts a stopped machine and waits for the agent itself.
Starting a machine blocks until the boot completes, which normally takes a few seconds. Pass ?detach=true to return 202 Accepted immediately and boot in the background, then poll GET /v1/machines/{id} until the state is started or error. Under a burst this keeps a client’s own request timeout from cancelling a start that is still queued. exec has the same option under a different name: background in the request body.
Stopping a machine preserves its stored state. Deleting it removes the machine. See the lifecycle page for billing and storage details.
Plan limits
Three ceilings apply to every tenant, and all three come from the plan the account is on rather than from the platform:
- How many machines you may hold: counted across every state, not just running ones, so a stopped machine still occupies a slot until it is deleted.
- How many may run at once: counted over
started,creatingandpausing, leaving out a source frozen as a branch base. - How large one machine may be: vCPU, memory, and disk, applied per machine at create time.
Read your own ceilings rather than assuming them. GET /v1/account returns
the ones that apply to you now as effectiveMaxMachines, effectiveMaxCpus, effectiveMaxMemoryMb and effectiveMaxDiskGb. The plan object beside them
carries the plan’s own maxConcurrentMachines, maxCpus, maxMemoryMb and maxDiskGb. On the free plan an account without a card is held to 4 vCPU per
machine, which effectiveMaxCpus shows, and to 4 running machines, which no
field reports:
curl -fsSL https://api.smolmachines.com/v1/account \
-H "Authorization: Bearer $SMOL_CLOUD_TOKEN" The published plans and their ceilings are listed on the pricing page. Limits differ between plans; the published per-unit rates do not, so moving up a published plan buys headroom rather than a different price. An Enterprise contract may carry its own rates.
Exceeding a ceiling is refused at create time with 422 and a body naming the
plan, the limit, and where to change it — nothing is started, so nothing is
charged:
machine count quota exceeded: your Standard plan allows 20 machines; upgrade at https://smolmachines.com/pricing
concurrency limit reached: your Standard plan allows 20 machines running at once; upgrade at https://smolmachines.com/pricing An account without a card on file runs up to 4 machines at once of up to 4 vCPU each, and its refusals say so; adding a card lifts both to the plan’s limits:
concurrency limit reached: an account without a card can run 4 machines at once; add a card at https://smolmachines.com/console/billing to run up to your plan's limit
machine size exceeds what an account without a card can run, up to 4 vCPU, 16 GB memory and 50 GB disk per machine (requested 8 vCPU, 8 GB); add a card at https://smolmachines.com/console/billing to use your plan's full limit TIP
A count refusal usually means machines were left behind rather than that the
ceiling is genuinely too low. Ephemeral work should delete its machine when it
finishes, and autoStopSeconds stops an idle machine but does not delete it —
a stopped machine still holds its slot.
Errors and request IDs
Use the HTTP status code first. Error bodies are RFC 9457 application/problem+json: a stable code, a detail sentence, and retryable, which is true for 408, 429, 502, 503 and 504; those also carry Retry-After in seconds. An exec whose machine stopped under it returns 503 with code machine_interrupted: the command may have partly run, and a retry runs it again. Two other refusals are worth retrying after a wait: a branch answers 409 “branches of this source made no progress …; retry” while it waits behind its siblings, and a create answers 422 “no ready
Common statuses include:
400: invalid request401: missing or invalid credentials403: insufficient scope404: resource not found or not owned by the caller409: conflicting resource402: billing or budget restriction422: quota, capacity, or validation constraint429: rate limit exceeded500: server error503: service or feature unavailable
Every response includes x-request-id. A safe client-provided ID is echoed; otherwise the service generates one. Record it with the status and response body when reporting a failed request.
Checkpoints and branches
Checkpoints capture any running machine, guest RAM and processes included, into durable storage: POST /v1/machines/{id}/checkpoints captures one, GET /v1/machines/{id}/checkpoints lists them, POST /v1/checkpoints/{id}/restore starts a new machine from one, and DELETE /v1/checkpoints/{id} (machine:delete) deletes one, answering 409 while a machine still references it. See Branches and checkpoints for the lifecycle and the CLI equivalents.
Branch routes are specialized cloud operations. They do not move a running local VM into cloud or provide a portable cross-architecture restore format. POST /v1/machines/{id}/snapshot is not implemented and returns 501; use a checkpoint to capture a running machine, or export a stopped machine’s disk to a .smolmachine artifact (POST /v1/machines/{id}/export, or smol cloud export).
GET /v1/capacity (machine:read) sizes a fan-out before it starts: it returns how many machines you have and how many are started, the memory configured across started machines, and, for each running branchable machine, its memory and its node’s branch headroom (nodeHeadroomMib). A branch its source’s node cannot hold is refused with a retryable 503 insufficient_capacity, whose body carries node.requiredMib, node.headroomMib and retryAfterSeconds.