A VM is a plain Linux machine on NVMe with one public IPv4, built from a stock image we keep patched. Nothing is installed on it that we chose — configuration is code you re-run. Every machine runs in our single site, in Los Angeles, US; there is no region field. For the walkthrough that gets you your first one, start with Onboarding.
Storage is documented here: a root disk is part of a VM, and a volume, though it can sit detached, is useful only in one of a VM's disk slots.
One site, Los Angeles, US. There is no region field and nothing to pick, and no second region to fail into.
AMD EPYC Milan (Zen 3). That is x86-64-v3 — AVX2 and everything below it,
no AVX-512. Pick binaries on that baseline; an x86-64-v4 build faults on
first use of an instruction the CPU does not have.
A 10 Gbps network, shared with other VMs. Egress is metered per GB and
priced in the price book (GET /v1/billing/prices); inbound is free.
Storage is redundant on root disks and volumes alike, so you do not need to build replication. That is not backup. It does nothing about a delete you issued yourself, which is what snapshots are for.
POST /v1/vms {"name":"web1","ram_mb":4096,"disk_gb":40,
"image":"ubuntu-lts","ssh_keys":["ssh-ed25519 AAAA..."]}
| Field | Minimum | Step | Notes |
|-------|---------|------|-------|
| ram_mb | 1024 | 1024 | max 262144 |
| disk_gb | 24 | 8 | max 8192; never resizable after creation |
The vCPU count is derived, not chosen: ceil(ram_gb / 2). A 1–2 GB VM
gets 1 vCPU, 4 GB gets 2, 16 GB gets 8. Omit cpu: the field is optional, and
a value disagreeing with the formula is 400 cpu_derived. The
price book (GET /v1/billing/prices) prices RAM and nothing for vCPU.
Resize with POST /v1/vms/{id}/actions {"type":"resize","ram_mb":8192}; the
vCPU count follows. Stop the VM first — a resize on a running VM is
409 vm_not_stopped. You can size down as well as up, but nothing in the
guest is adjusted for you: bring your services' own memory settings down
before you shrink the machine, or they are killed on the way back up. The
root disk is never resizable — see below.
They differ in speed as much as in lifetime, and speed is usually what decides where something goes.
A database on the root with its backups on a volume is the normal shape.
disk_gb is the one decision you cannot change later. Oversize it — the fast
tier prices above bulk storage (both are in the price book,
GET /v1/billing/prices), and the extra GB still cost less than a recreate —
and treat the root as disposable: a VM whose root is full or too small is
recreated, not resized.
Root disks start at 24 GB and move in 8 GB steps. Round up.
GET /v1/volumes yours, attached or not
GET /v1/volumes/{id}
POST /v1/volumes {"name":"data1","size_gb":500}
POST /v1/volumes/{id}/actions {"type":"attach","vm_id":"vm_..."}
POST /v1/volumes/{id}/actions {"type":"detach"}
POST /v1/volumes/{id}/actions {"type":"resize","size_gb":1000}
DELETE /v1/volumes/{id}
stopped (409 vm_not_stopped): unmount in-guest, stop, detach.409 volume_in_snapshots and names those snapshots in the
snapshots extra; delete them first. A snapshot can restore a volume only
while the volume is still on the VM that took it, so moving a volume means
giving up the snapshots that include it. Snapshots taken before you attached
it don't count.size_gb must be
a multiple of 500 on create and on grow.growpart + resize2fs for ext4, xfs_growfs for xfs). No stop, no
detach.409 must_detach_first), and it
destroys the data.409 vm_slots_full. If you need more, add a VM.409 volume_moved says the
volume was attached, detached or deleted between your read and your write:
re-read GET /v1/volumes/{id} and decide again, because a blind retry will
not fix it. 409 volume_already_grown says another resize passed the size
you asked for; retry only with a larger number. Neither applied anything.A new volume arrives unformatted, and what you do with it once is
in-guest: find it with lsblk (a disk with no partitions), make a filesystem
on it, then read its UUID with blkid.
Mount it by UUID in /etc/fstab, never by device name. The kernel names
disks in the order it finds them, and that order changes when you attach
another volume or recreate the machine — so a
/dev/sdb in fstab eventually mounts the wrong disk or none. Add nofail
while you are there: without it a volume that is missing at boot stops the
machine before SSH comes up, and there is no console to fix that from.
GET /v1/vms/{id}/snapshot list, newest first
POST /v1/vms/{id}/snapshot {"name":"before-upgrade"}
DELETE /v1/vms/{id}/snapshot/{name}
POST /v1/vms/{id}/snapshot/{name}/rollback newest only; VM must be stopped
GET /v1/billing/prices).
Billing is per snapshot, so GET /v1/usage?group_by=resource tells you
which one to delete. Deleting
it stops the charge within the minute.POST /v1/vms/{id}/snapshot) cover the root **and all
attached volumes at once. They're crash-consistent** — the same thing
your filesystem sees after a power cut, which ext4/xfs and any crash-safe
database recover from cleanly. Memory isn't captured, so a rollback is a
boot, not a resume.409 snapshot_volumes_changed naming it.
Detach it, or snapshot again and roll back to that. The volumes a snapshot
includes can't have gone anywhere: they stay attached until it is deleted.Every VM gets one public IPv4 from our own pool at creation. It stays with that VM until the VM is destroyed, then returns to the pool. No endpoint reassigns an address.
It bills on the ip-hours meter, priced in the price book
(GET /v1/billing/prices), for as long as you hold it, including while the VM
is stopped. Deleting the VM returns it to the pool and ends the charge.
So put a DNS name in front of every VM rather than writing its address
down anywhere. Keep the TTL low (60s is plenty) and a recreate costs a minute;
both VMs exist during the cutover, so you can verify the new one before the
old address goes away. If you need a specific address moved, open a ticket
(POST /v1/support/tickets).
Your addresses are in GET /v1/vms — each machine carries its own ip. There
is no separate address list.
The only rule we enforce is anti-spoof — a VM may emit only from its own
IP and MAC. Nothing else is filtered and there is no firewall API. Run
nftables or ufw in-guest; you own the policy.
Your VM's network reaches the Internet and nothing of ours. Everything arriving on your ports is yours to filter.
Two things the platform does decide for you:
:25 is blocked — at the network, upstream of your VM, so
no in-guest configuration changes it. You cannot run your own mail server
that delivers to receiving servers directly; send through the
relay instead. Submission ports (587, 465) outbound to
third parties are not blocked.Pass at least one public key at creation — there is no password, no console,
and no web reset path. Log in as ubuntu.
**A firewall or sshd change you haven't tested can brick the VM permanently**, with no out-of-band door to fix it through. Test changes before you commit them, and treat a locked-out VM as a recreate, not a rescue.
GET /v1/vms yours, with power read live
GET /v1/vms/{id}
POST /v1/vms/{id}/actions {"type":"start"|"stop"|"reboot"|"resize"}
DELETE /v1/vms/{id}
GET /v1/vms/{id}/events what happened, in order
A VM carries two separate fields, and there is no state: status is the
platform's view of the record (active), and power is read live from the
hypervisor on every request — running, stopped, or unknown when the
hypervisor could not be reached, which health: unreachable says at the same
time. power is never cached.
Creation blocks, and leaves the VM stopped: POST /v1/vms builds the
machine and answers 201 with power: "stopped", typically in about a
second. If it can't, you get the failure directly — 409 create_failed,
carrying the hypervisor's own error text. Two refusals arrive before the
build starts rather than from it: 409 no_capacity_available when there is no
room for that much RAM right now, which is not a quota and not raised by
raising one, and 409 ip_pool_exhausted when no address is free.
Creating and starting are separate calls. Attach before the first boot if your setup automation expects the disk to be there:
POST /v1/vms {...} → 201, stopped
POST /v1/volumes {"name":"data1","size_gb":500} → 201, detached
POST /v1/volumes/{id}/actions {"type":"attach","vm_id":"vm_..."}
POST /v1/vms/{id}/actions {"type":"start"}
A failed create is rolled back: the partial VM is destroyed and its IP
returned, so there is nothing to clean up and nothing holding quota. Fix what
detail says and POST again.
Every action works the same way: it runs while you wait and the response
is the outcome — 200 when it's done, or the error itself (the hypervisor's
own text) when it isn't. There is nothing to poll; if a call fails, fix what
detail says and issue it again. GET /v1/vms/{id}
reports power live from the hypervisor whenever you want to look. stop is
graceful with a ~20-second bound (pass "force": true for an immediate hard
stop); start and stop are both idempotent, so a machine already in the
state you asked for answers 200 rather than an error.
Stopping a VM stops its RAM and vCPU metering and nothing else: the root
disk, the address, its snapshots and its volumes bill whether it runs or not,
so a machine you are done with is deleted rather than parked.
GET /v1/vms/{id}/events records what happened, in order — it is where to
look when a create fails. A VM that was running when the hypervisor under it
rebooted comes back up on its own.
Deleting a VM destroys the VM and its root disk, and releases its IP. It
destroys nothing else and tidies up nothing: delete the VM's snapshots first
(newest first), then stop it and detach its volumes, then delete it. Until you
do, DELETE answers 409 vm_has_snapshots or
409 vm_has_volumes and lists what is still there. The order matters — a
volume can't be detached while one of the VM's snapshots includes it.
Stock images only; no custom image upload. GET /v1/images is the catalog and
the only source of truth for what is offered — Ubuntu LTS today, so check it
rather than assuming. Pin an img_... id for reproducibility, or use a name
alias like ubuntu-lts to follow the newest version under that name.
Images have no status and no expiry date. Everything listed is usable, and
a withdrawn image stops being listed, so GET /v1/images is the whole answer.
Anything custom happens in-guest after boot.
At balance ≤ 0, mutating calls stop (402 credit_cutoff) but **running VMs
are not killed** and disks keep their data. There is a grace period before
VMs are stopped and a longer one before any disk is deleted; top up inside it
and everything resumes. The timings:
API conventions.