---
title: "Create a VM"
method: POST
path: "/v1/orgs/{orgId}/vms/create"
tags: ["VMs"]
---

# Create a VM

`POST /v1/orgs/{orgId}/vms/create`

## Path parameters

- `orgId` string, required

## Request body

- CreateVmRequest
  - `allowFallback` boolean
  - `cloudInit` string — Startup script run inside the guest the first time this VM boots. Must begin with a shebang (e.g. "#!/bin/bash"); a raw cloud-config document is rejected. Runs once per disk, not once per boot: a stop/start keeps the disk and does not re-run it, while a VM that lands on a fresh disk does. It runs as root once the network and your SSH keys are up, and its output goes to the VM's logs. A script that fails does not stop the VM from starting or from being billed, so check the logs rather than assuming it succeeded.
  - `containerCommand` string[]
  - `containerPort` integer
  - `diskSizeGb` integer — Boot disk in GB. Omit to get the default, which for a GPU VM is 100 GB per GPU. That default is not a floor: a smaller size is accepted down to the platform minimum (minDiskGb on GET /v1/gpu-availability). The ceiling is the node's per-GPU disk share times gpuCount (perGpu.diskGb on its offer), so a one-GPU VM cannot take a whole node's VM dir, and the size must also be free on that node right now.
  - `envVars` object
  - `gpuCount` integer
  - `gpuModelId` string
  - `guestMemMb` integer — Guest RAM for a GPU VM, in MiB. Omit for auto, which takes the tightest node that can give a full per-GPU share. At least 65% of the VRAM the VM is placed against (the catalog's floorPerGpu; a smaller request is raised to it), and at most some node's per-GPU share times gpuCount: a request above every node's share is rejected with VM_SHAPE_NOT_PLACEABLE, and one inside a share that no node has free right now with INSUFFICIENT_HOST_CAPACITY. Starting the VM after a stop, or re-dispatching it after its job is lost, starts it on its own node at no more than that node's current share (see requestedGuestMemMb).
  - `healthCheckPath` string
  - `healthCheckPort` integer
  - `healthChecks` string[]
  - `imageUrl` string
  - `kernelModules` string[]
  - `name` string, required
  - `nodeId` string — Place this VM on a specific node. Required to use capacity reserved to your org; omit to use the public pool. There is no fallback: if the node has no room for this GPU count the request is refused rather than placed elsewhere. A node you have no rights on reads as not found.
  - `preferredRegions` string[]
  - `privileged` boolean
  - `public` boolean — true = open endpoint (no data-plane auth); default false requires an org API key
  - `registryCredentialId` string
  - `resourceSize` string — CPU-only VM machine size (the tier picker). Sending it alongside gpuCount is rejected with RESOURCE_SIZE_NOT_APPLICABLE: a GPU VM is sized from the host capacity its GPUs come with, not from this table.
  - `sshKeyIds` string[], required — Org SSH key ids injected into the guest. At least one is required and every id must name a live key of this org: the data-plane gateway authorizes guest SSH against exactly this list, so a VM created without one has no SSH access.
  - `templateId` string
  - `tier` string

## Response `200`

Created

- VmSummary
  - `capabilityTier` string — compute-fabric capability tier (e.g. "pod"); empty for a plain VM
  - `computeClass` string — compute-fabric purchase option (on_demand; pods created before the rename carry the legacy reserved or interruptible values); empty for a plain VM
  - `createdAt` string
  - `diskSizeGb` integer
  - `endpointUrl` string
  - `gpuCount` integer
  - `gpuManufacturer` string — GPU vendor from the catalog, the same value as GpuModel.manufacturer (NVIDIA, AMD); absent on a CPU-only row
  - `gpuModelId` string
  - `gpuModelName` string
  - `id` string, required
  - `imageUrl` string — tenant container image (pods)
  - `managedBy` string — platform owner of this row ("serving" = inference serving replica, read-only on customer surfaces); empty for a customer-launched VM
  - `name` string, required
  - `organizationId` string, required
  - `pricePerHourCents` integer
  - `provisioningStage` string
  - `public` boolean — true = open endpoint (no data-plane auth); false = require an org API key
  - `resourceSize` string
  - `serviceType` string — managed-service tag; empty for a plain VM
  - `status` string, required — deploying, running, stopped, failed, terminated, or interrupted; a row can also carry the queued status pending. interrupted means it served and is not serving now: it keeps its node, GPU and disk, and is not billed. A VM is interrupted while its node is offline and resumes when the node returns; a pod is interrupted for that and for losing its supervisor or its job on a live node, which the platform recovers by re-submitting it. After 7 days interrupted a VM is stopped (disk kept) and a pod is deleted.
  - `statusReason` string — Cause of the current status. The machine-readable values are "node_lost" (the host went offline and is expected back, so the disk is preserved), "node_removed" (the host was decommissioned, so the disk is gone and a restart deploys fresh elsewhere), "vm_exited", "user_stopped", "user_terminated", "user_restarted", "spot_reclaimed_by_capacity_owner", "spot_preempted_for_reservation_tenant", "auto_stopped_insufficient_balance", and the prefix "container failed to start: " followed by the container runtime's own message. Anything else is free text for a human to read.
  - `tier` string
  - `volumeGb` integer — pods only: size in GB of the pod's persistent /workspace volume.

## Other responses

- `400` — The request is invalid
- `401` — Missing or invalid API key
- `403` — API key lacks the required scope
- `409` — The request conflicts with existing state
- `503` — The request is valid, but there is no capacity for it right now. A create, fork, or cluster create or scale answers NODE_AT_CAPACITY, INSUFFICIENT_GPU_CAPACITY, INSUFFICIENT_CPU_CAPACITY, INSUFFICIENT_HOST_CAPACITY or INSUFFICIENT_IP_CAPACITY: pick another shape, GPU model or node, or try later, rather than repeating the same request in a loop. A restart answers VM_NODE_BUSY, POD_NODE_BUSY or INSUFFICIENT_IP_CAPACITY: the workload is pinned to the node that holds its disk or volume, and the refusal clears when that node frees capacity. The Retry-After header is deprecated.

## Changes

- **2026-09-21** `e169795b5fe8` — 1 info
  - added the optional property `gpuManufacturer` to the response with the `200` status
- **2026-09-17** `552210fd5afe` — 1 info
  - added the new optional request property `guestMemMb`
- **2026-09-05** `383d5864ac75` — 2 breaking, 1 info
  - the request property `sshKeyIds` became required
  - the `sshKeyIds` request property's minItems was increased to `1`
  - added the optional property `volumeGb` to the response with the `200` status
- **2026-08-17** `7e2fe85577f4` — 1 info
  - added the optional property `managedBy` to the response with the `200` status
- **2026-08-16** `ed76b41e8006` — 1 info
  - added the non-success response with the status `503`

[Full history](https://skmtc.dev/openrelay/apis/openrelay-api/changes/v1/orgs/:orgId/vms/create/post.md)

---

[API](https://skmtc.dev/openrelay/apis/openrelay-api.md) · [All operations](https://skmtc.dev/openrelay/apis/openrelay-api/llms.txt) · [OpenAPI document](https://skmtc.dev/openrelay/apis/openrelay-api/revisions/24a11ebdab5e?raw)
