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.
Endpoints
Section titled “Endpoints”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/vmslists the collection,POST /api/v1/vmscreates one.GET /api/v1/vms/12reads an item by its path parameter;PATCHupdates,DELETEremoves.- 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.
Examples
Section titled “Examples”List the virtual machines:
curl -s http://<your-server>/api/v1/vms -H "Authorization: Bearer $TOKEN"Create a folder:
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:
curl -s -X POST http://<your-server>/api/v1/vms/12/start \ -H "Authorization: Bearer $TOKEN"Read the live catalog:
curl -s http://<your-server>/api/v1/catalog -H "Authorization: Bearer $TOKEN"Reads cover every server
Section titled “Reads cover every server”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": []}Calling another server
Section titled “Calling another server”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.
Renamed operations
Section titled “Renamed operations”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:
| Before | Now |
|---|---|
peer_id argument | server_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_id | to_server_id |
set_role_priority peer_ids | server_ids |
result fields peer_id, holder_peer_id, source_peer_id, priority_peer_ids, started_by_peer_id | server_id, holder_server_id, source_server_id, priority_server_ids, started_by_server_id |
backup destination on another server: type peer, peer_id, peer_name | type server, server_id, server_name |
wake and power-off results: target_peer_id, display_name | server_id, server |
bitsmith peer list, peer wake, peer power-off | bitsmith server list, server wake, server power-off |
bitsmith vm move-to-peer, lxc move-to-peer | bitsmith 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 resume | bitsmith 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 unfreeze | bitsmith 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-update | bitsmith 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}/subfolders | create_subfolders with a one-element names list; POST /api/v1/folders/{folder_id}/subfolders/batch |
bitsmith folder mkdir <id> --name <name>, folder mkdir-batch | bitsmith folder mkdir <id> --names '["<name>"]' |
suspend_sandbox, resume_sandbox; POST /api/v1/sandboxes/{id}/suspend, /resume | control_sandbox with action suspend or resume; POST /api/v1/sandboxes/{id}/actions |
bitsmith sandbox suspend, sandbox resume | bitsmith 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}/docs | get_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 paths | update_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-set | bitsmith 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": [...]} |
Calls that wait for approval
Section titled “Calls that wait for approval”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:
| Status | code | Meaning |
|---|---|---|
403 | approval_rejected | Someone rejected the call. Don’t retry it. |
403 | approval_timed_out | Nobody answered in time. |
403 | approval_token_deleted | The token was deleted while the call waited. |
408 | approval_cancelled | The client stopped waiting. |
503 | approval_interrupted | Bitsmith stopped or restarted while the call waited. |
503 | approval_unavailable | Bitsmith 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..."}Errors
Section titled “Errors”Every error comes back as {"error": "..."} with a status code:
| Status | Meaning |
|---|---|
400 | The body is not a JSON object, or an argument does not fit the schema. |
401 | No token, or a token that does not match. |
403 | The token lacks the scope the endpoint needs, or the call was refused approval (see above). |
404 | The path is not an endpoint this server offers, or the resource it names does not exist. |
405 | Wrong method for the path; the Allow header lists the valid ones. |
409 | The resource’s state blocks the call, such as changing a running VM. |
413 | The request body is over 1 MiB. |
429 | The server has too little free memory for the new VM or container right now. |
500 | The call failed in a way that has no specific code; the error text names what happened. |
502 | The 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. |
503 | A host dependency such as the VM runtime is unavailable. |
How calls are recorded
Section titled “How calls are recorded”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.