Skip to content

API

The API is Bitsmith’s stable HTTP surface: REST endpoints for the resources the server manages: virtual machines, LXC containers, apps, folders, connected storage, the reverse proxy, and sandboxes. MCP exposes the same operations to agent clients; the API complements it for integrations that are not MCP clients: scripts, server-side services, and anything that can send a bearer token. Both transports use the same agent tokens, the same scopes, and the same activity log, so a token you create works on both with no extra setup.

The API lives under /api/v1/ and authenticates with Authorization: Bearer <token>, where the token is one you created on the Access tokens page (System, Agent access).

Resources follow standard REST conventions:

  • GET /api/v1/vms lists the collection, POST /api/v1/vms creates one.
  • GET /api/v1/vms/12 reads an item by its path parameter; PATCH updates, DELETE removes.
  • Actions live on the item as sub-paths: POST /api/v1/vms/12/start, POST /api/v1/vms/12/exec, POST /api/v1/vms/12/template.
  • Every domain has the same shape: /api/v1/lxc, /api/v1/docker, /api/v1/folders, /api/v1/locations, /api/v1/proxy, /api/v1/sandboxes, plus /api/v1/system, /api/v1/activity, /api/v1/servers, and /api/v1/tasks.

Path parameters are part of the URL and always named id or the natural sub-resource name (image_id, sub_path). A path parameter fills the operation’s argument of the same name; for example, the {id} in /api/v1/vms/{id} is the VM ID. The rest of an operation’s arguments go in the JSON body for POST, PATCH, and PUT, and in the query string for GET and DELETE. server_id works in either place, wherever the operation accepts it.

The full listing for this release (every endpoint, its method and path, its summary, its scope, and the JSON schema of its arguments) is generated in the API reference. The catalog is defined by your server build, so GET /api/v1/catalog remains the authoritative source for what a given server can do: read it when in doubt.

The plain-HTTP listener answers these paths directly rather than redirecting to HTTPS, for the same reason it does for /mcp: plain curl and integration code cannot click through a self-signed certificate.

List the virtual machines:

Terminal window
curl -s http://<your-server>/api/v1/vms -H "Authorization: Bearer $TOKEN"

Create a folder:

Terminal window
curl -s -X POST http://<your-server>/api/v1/folders \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"storage_id": "tank", "name": "agent-inbox"}'

Start VM 12:

Terminal window
curl -s -X POST http://<your-server>/api/v1/vms/12/start \
-H "Authorization: Bearer $TOKEN"

Read the live catalog:

Terminal window
curl -s http://<your-server>/api/v1/catalog -H "Authorization: Bearer $TOKEN"

In a cluster, a read answers for every server. A list, or a read by id, comes back as {"items": [...], "servers": [...]}: every item carries the server_id and server it came from, and servers names any server that was offline or failed, so a partial answer never looks complete. Status reads, such as GET /api/v1/system/health, come back as {"servers": [...]} with one entry per server. A read by id that no server holds answers 404, naming the servers it searched.

{
"items": [
{"vmid": 100, "name": "web", "status": "running", "server_id": "4f2c…", "server": "garage"},
{"vmid": 100, "name": "nas", "status": "stopped", "server_id": "9a1e…", "server": "attic"}
],
"servers": []
}

Every operation that acts on one server accepts an optional server_id argument, in the body or the query string (GET /api/v1/servers lists the servers). On a read it narrows the answer to that server. On a change it runs the change there, with your token’s scopes forwarded across; leave it out and the change runs on the server you called. A change’s result names the server it ran on in server_id, so a background task it starts can be followed and cancelled there. A change goes only to a server on the same Bitsmith version as the one you called; another server answers 409 until it has updated, which it does by itself.

The reverse proxy endpoints take no server_id. One server runs your sites, and every proxy call goes there by itself, whichever server you called. A task one of them starts runs on that server, and the result names it in server_id.

Names that said peer now say server. A call that uses an old name fails with the new one in its message, and nothing runs:

BeforeNow
peer_id argumentserver_id
GET /api/v1/peers (list_peers, self_peer_id)GET /api/v1/servers (list_servers, self_server_id)
POST /api/v1/peers/{peer_id}/wake (wake_peer, target_peer_id)POST /api/v1/servers/{server_id}/wake (wake_server, server_id)
POST /api/v1/peers/{peer_id}/power-off (power_off_peer, target_peer_id)POST /api/v1/servers/{server_id}/power-off (power_off_server, server_id)
POST /api/v1/vms/{id}/move-to-peer (move_virtual_machine_to_peer, target_peer_id)POST /api/v1/vms/{id}/move (move_virtual_machine, to_server_id, optional from_server_id)
POST /api/v1/lxc/{id}/move-to-peer (move_lxc_container_to_peer, target_peer_id)POST /api/v1/lxc/{id}/move (move_lxc_container, to_server_id, optional from_server_id)
POST /api/v1/lxc/{id}/move (move_lxc_container_to_storage)POST /api/v1/lxc/{id}/move-storage
POST /api/v1/proxy/move (move_proxy_to_peer, peer_id)POST /api/v1/proxy/move (move_proxy, server_id)
backup target_peer_idto_server_id
set_role_priority peer_idsserver_ids
result fields peer_id, holder_peer_id, source_peer_id, priority_peer_ids, started_by_peer_idserver_id, holder_server_id, source_server_id, priority_server_ids, started_by_server_id
backup destination on another server: type peer, peer_id, peer_nametype server, server_id, server_name
wake and power-off results: target_peer_id, display_nameserver_id, server
bitsmith peer list, peer wake, peer power-offbitsmith server list, server wake, server power-off
bitsmith vm move-to-peer, lxc move-to-peerbitsmith vm move, lxc move (--from, --to)
bitsmith lxc move (to another storage pool)bitsmith lxc move-storage
start_virtual_machine, stop_virtual_machine, suspend_virtual_machine, resume_virtual_machine; POST /api/v1/vms/{id}/start (and /stop, /suspend, /resume)control_virtual_machine with action start, stop, suspend or resume; POST /api/v1/vms/{id}/actions
bitsmith vm start, vm stop, vm suspend, vm resumebitsmith vm control <id> --action <action>
POST /api/v1/lxc/{id}/app/update, /app/retry (bitsmith lxc app-update, lxc app-retry)POST /api/v1/app-catalog/update, /app-catalog/retry with lxcid or vmid in the body (bitsmith catalog update, catalog retry with --lxc or --vm)
start_lxc_container, stop_lxc_container, restart_lxc_container, freeze_lxc_container, unfreeze_lxc_container; POST /api/v1/lxc/{id}/start (and /stop, /restart, /freeze, /unfreeze)control_lxc_container with action start, stop, restart, freeze or unfreeze; POST /api/v1/lxc/{id}/actions
bitsmith lxc start, lxc stop, lxc restart, lxc freeze, lxc unfreezebitsmith lxc control <id> --action <action>
add_proxy_domain (name), update_proxy_domain; POST /api/v1/proxy/domains, PATCH /api/v1/proxy/domains/{domain}upsert_proxy_domain with mode create or update (domain replaces name); PUT /api/v1/proxy/domains/{domain}
add_proxy_entry, update_proxy_entry; POST /api/v1/proxy/domains/{domain}/entries, PATCH /api/v1/proxy/domains/{domain}/entries/{subdomain}upsert_proxy_entry with mode; PUT /api/v1/proxy/domains/{domain}/entries/{subdomain}
add_proxy_dns_rewrite, update_proxy_dns_rewrite; POST /api/v1/proxy/dns/rewrites, PATCH /api/v1/proxy/dns/rewrites/{domain}upsert_proxy_dns_rewrite with mode; PUT /api/v1/proxy/dns/rewrites/{domain}
bitsmith proxy domain-add, domain-update, entry-add, entry-update, rewrite-add, rewrite-updatebitsmith proxy domain-upsert, entry-upsert, rewrite-upsert with --mode create or --mode update
create_backup_schedule, update_backup_schedule; POST /api/v1/backups/schedules, PATCH /api/v1/backups/schedules/{schedule_id}upsert_backup_schedule: no schedule_id creates, a schedule_id changes that schedule; PUT /api/v1/backups/schedules
bitsmith backup create, backup update <id>bitsmith backup upsert, backup upsert --id <id>
create_subfolder (name); POST /api/v1/folders/{folder_id}/subfolderscreate_subfolders with a one-element names list; POST /api/v1/folders/{folder_id}/subfolders/batch
bitsmith folder mkdir <id> --name <name>, folder mkdir-batchbitsmith folder mkdir <id> --names '["<name>"]'
suspend_sandbox, resume_sandbox; POST /api/v1/sandboxes/{id}/suspend, /resumecontrol_sandbox with action suspend or resume; POST /api/v1/sandboxes/{id}/actions
bitsmith sandbox suspend, sandbox resumebitsmith sandbox control <id> --action <action>
create_runner_pool, update_runner_pool; POST /api/v1/runner-pools, PATCH /api/v1/runner-pools/{pool_id}upsert_runner_pool: no pool_id creates, a pool_id changes that pool; PUT /api/v1/runner-pools
bitsmith runner create, runner update <id>bitsmith runner upsert, runner upsert --id <id>
get_vm_docs, get_lxc_docs, get_docker_app_docs; GET /api/v1/vms/{id}/docs, /lxc/{id}/docs, /docker/{app_id}/docsget_resource_docs with kind vm, lxc or docker and id (a string); GET /api/v1/resource-docs/{kind}/{id}
update_vm_docs, update_lxc_docs, update_docker_app_docs; PUT on the same three pathsupdate_resource_docs with kind and id; PUT /api/v1/resource-docs/{kind}/{id}
bitsmith vm docs, vm docs-set, lxc docs, lxc docs-set, docker docs, docker docs-setbitsmith docs get <kind> <id>, docs set <kind> <id>
A list’s answer: a bare array; a read by id: the object{"items": [...], "servers": [...]}, every item naming its server; status reads {"servers": [...]}

An endpoint that changes something ("read_only": false in the catalog, which also marks it "waits_for_approval": true) waits until a person approves the call in Bitsmith, for up to 15 minutes, before it runs. Keep the request open: curl waits by default, but an HTTP client with a short timeout gives up and the call is refused. See MCP agent access for how approving works.

A refused call answers with a code next to the error, and nothing ran:

StatuscodeMeaning
403approval_rejectedSomeone rejected the call. Don’t retry it.
403approval_timed_outNobody answered in time.
403approval_token_deletedThe token was deleted while the call waited.
408approval_cancelledThe client stopped waiting.
503approval_interruptedBitsmith stopped or restarted while the call waited.
503approval_unavailableBitsmith could not check the call’s approval.
{"error": "approval_rejected: the owner rejected this call in Bitsmith; it did not run", "code": "approval_rejected", "approval_id": "6f1c..."}

Every error comes back as {"error": "..."} with a status code:

StatusMeaning
400The body is not a JSON object, or an argument does not fit the schema.
401No token, or a token that does not match.
403The token lacks the scope the endpoint needs, or the call was refused approval (see above).
404The path is not an endpoint this server offers, or the resource it names does not exist.
405Wrong method for the path; the Allow header lists the valid ones.
409The resource’s state blocks the call, such as changing a running VM.
413The request body is over 1 MiB.
429The server has too little free memory for the new VM or container right now.
500The call failed in a way that has no specific code; the error text names what happened.
502The call was routed to a paired server that could not run it. When that server refuses the call, you get its own status and message instead, as if you had called it directly.
503A host dependency such as the VM runtime is unavailable.

Every mutating call writes one row to the Activity log, attributed to the token’s name, exactly as an MCP call would. The row’s metadata records which transport it arrived on: mcp, api, or cluster (the server that ran a call forwarded to it). Read-only calls are not recorded.