API Reference

Betawings REST API — v1

The Betawings API lets you orchestrate Firecracker MicroVMs. VMs boot in under a second from a snapshot and are isolated at the hardware-virtualisation level — more secure than containers, more efficient than traditional VMs.

All endpoints return JSON. Error responses always include an error field.

Base URL

https://api.betawings.com/api/v1

Response format

All successful responses are application/json. Errors use standard HTTP status codes with a JSON body:

{"error": "vm must be stopped before resizing cpu/memory"}

Authentication

Every request to /api/v1/* must include a bearer token in the Authorization header.

Authorization: Bearer <your-token>

Roles

Three org-scoped built-in roles are currently available to users.

org-admin
Full access to VMs within their org. Can create, manage, and revoke org-scoped tokens.
creator
Can create and manage VMs within their org. Cannot manage tokens.
reader
Read-only access to VMs within their org.

Creating a token

curl -X POST https://api.betawings.com/api/v1/tokens/ \
  -H "Authorization: Bearer <org-admin-token>" \
  -H "Content-Type: application/json" \
  -d '{"name":"ci-deploy","role":"creator","org_id":"<org-id>"}'

The raw token is only returned once at creation time — store it immediately.

The VM Object

FieldTypeDescription
idstringUnique VM identifier (16-char hex).
namestringHuman-readable name. Must be unique. Used as the subdomain under *.onbw.net.
statusstringrunning stopped deploying error snapshotting
imagestringBase disk image name the VM was created from.
cpu_countintNumber of vCPUs (1–32).
memory_mbintRAM in mebibytes (64–65536).
disk_mbintDisk size in MB.
ip_addressstringInternal tap IP of the VM (10.200.x.x). Only set when running.
node_idstringNode the VM lives on. "local" means it runs on the control plane host.
idle_timeout_secondsintSeconds of inactivity before the VM is auto-snapshotted and stopped. Default 300.
has_snapshotboolWhether a Firecracker snapshot exists. If true, the next start is a fast (~1 s) resume.
ssh_key_fingerprintstringSHA-256 fingerprint of the injected SSH public key.
terminal_urlstringURL to the in-browser terminal (requires VM to be running with TLS cert obtained).
tagsobjectArbitrary key/value metadata. Example: {"env":"prod","team":"backend"}.
org_idstringOrganisation the VM belongs to.
created_atstringRFC 3339 timestamp.
started_atstringRFC 3339 timestamp of most recent start. null if never started.
stopped_atstringRFC 3339 timestamp of most recent stop.
last_activity_atstringTimestamp of last reported activity (used by idle checker).
error_msgstringSet when status is error. Describes what failed.

VM lifecycle

StateWhat it meansNext actions
deployingVM is being provisioned from the base image (remote node only).Poll Get VM; transitions to stopped or error.
runningFirecracker process is active; VM is accepting traffic.Stop, Snapshot, Destroy, read Logs.
snapshottingFirecracker memory snapshot is being written to disk.Wait; transitions to stopped.
stoppedVM is not running. If has_snapshot: true, next Start resumes in ~300 ms. Otherwise cold boot (~10–20 s).Start, Rebuild, Resize, Migrate, Destroy.
errorLast operation failed. Check error_msg and VM Logs.Start (retry), Rebuild, Destroy.

Virtual Machines

Create VM

POST /api/v1/vms/

Creates a new VM and starts it immediately. The VM is provisioned from the specified base image. If multi-node is configured, the CP auto-selects the node with the most free memory unless node_id is specified.

When targeting a remote node, provisioning is asynchronous — the API returns immediately with status: "deploying". Poll Get VM until status becomes "stopped" or "error".

Request body

FieldRequiredDescription
namerequiredUnique VM name. Used as subdomain (name.onbw.net). Lowercase alphanumeric and hyphens.
imagerequiredBase image name (see List Images). Example: "demo-44322".
cpusrequiredNumber of vCPUs. Integer 1–32.
memory_mbrequiredRAM in MB. Integer 64–65536.
ssh_public_keyoptionalSSH public key to inject into /root/.ssh/authorized_keys. OpenSSH format.
idle_timeout_secondsoptionalSeconds idle before auto-snapshot+stop. Default: 300. Range: 10–86400.
org_idoptionalOrg to create the VM in. Cloud-admin only; org-scoped callers are bound to their org automatically.
node_idoptionalTarget node ID, or "local" to force the CP host. Omit to auto-select.
curl -X POST https://api.betawings.com/api/v1/vms/ \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "my-project",
    "image": "demo-44322",
    "cpus": 1,
    "memory_mb": 512,
    "ssh_public_key": "ssh-ed25519 AAAA... you@host",
    "idle_timeout_seconds": 600
  }'
{
  "id":                  "3f9a1c2b4e5d6f7a",
  "name":                "my-project",
  "status":              "running",
  "image":               "demo-44322",
  "cpu_count":           1,
  "memory_mb":           512,
  "disk_mb":             4096,
  "ip_address":          "10.200.3.2",
  "idle_timeout_seconds":600,
  "has_snapshot":        false,
  "ssh_key_fingerprint": "SHA256:abc123...",
  "terminal_url":        "https://my-project.onbw.net/terminal/<token>/",
  "node_id":             "local",
  "created_at":          "2026-07-11T10:00:00Z",
  "started_at":          "2026-07-11T10:00:01Z"
}

List VMs

GET /api/v1/vms/

Returns all VMs the caller can access. Org-scoped callers see only their org's VMs; cloud-admin sees all.

Query parameters

ParamDescription
statusFilter by status. Values: running, stopped, deploying, error.
tagFilter by tag in key:value format. Example: ?tag=env:prod.
curl https://api.betawings.com/api/v1/vms/?status=running \
  -H "Authorization: Bearer $TOKEN"

Get VM

GET /api/v1/vms/{id}

Returns a single VM by ID.

curl https://api.betawings.com/api/v1/vms/3f9a1c2b4e5d6f7a \
  -H "Authorization: Bearer $TOKEN"

Resize & Rename VM

PATCH /api/v1/vms/{id}

Updates one or more mutable VM properties. All fields are optional — include only what you want to change.

Changing cpus or memory_mb requires the VM to be stopped. Growing disk_mb also requires stopped status; shrinking is not allowed.

FieldDescription
nameNew name. Must be globally unique. Takes effect immediately without a restart.
cpusNew vCPU count (1–32). VM must be stopped.
memory_mbNew RAM in MB (64–65536). VM must be stopped.
disk_mbNew disk size in MB (1–102400). Grow-only. VM must be stopped.
tagsReplace the full tag set. Pass an empty object to clear all tags.
curl -X PATCH https://api.betawings.com/api/v1/vms/3f9a1c2b4e5d6f7a \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"cpus": 2, "memory_mb": 1024, "tags": {"env": "prod"}}'

Start VM

POST /api/v1/vms/{id}/start

Starts a stopped VM. If a Firecracker snapshot exists (has_snapshot: true), the VM resumes from it in ~1 second. Otherwise it cold-boots (~10–20 seconds). Returns the updated VM object.

curl -X POST https://api.betawings.com/api/v1/vms/3f9a1c2b4e5d6f7a/start \
  -H "Authorization: Bearer $TOKEN"

Stop VM

POST /api/v1/vms/{id}/stop

Stops a running VM. The Firecracker process is sent SIGKILL. Any unsaved in-memory state is lost; the disk (rootfs.ext4) is fully preserved.

curl -X POST https://api.betawings.com/api/v1/vms/3f9a1c2b4e5d6f7a/stop \
  -H "Authorization: Bearer $TOKEN"

Snapshot VM

POST /api/v1/vms/{id}/snapshot

Creates a Firecracker memory snapshot of a running VM. The next start will restore from this snapshot (~1 s) rather than cold-booting. By default the VM is stopped after the snapshot; pass keep_running: true to leave it running (useful for periodic checkpoints).

Firecracker snapshots are host-specific. They encode CPU register state in the current host's microarchitecture format and cannot be migrated to a different physical host. If you migrate the VM, the snapshot is discarded automatically and the next boot is a cold boot.

FieldDescription
keep_runningBoolean. Default false — VM is stopped after snapshot. Set true to keep it running.
curl -X POST https://api.betawings.com/api/v1/vms/3f9a1c2b4e5d6f7a/snapshot \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"keep_running": true}'

Rebuild VM

POST /api/v1/vms/{id}/rebuild

Replaces the VM's disk with a fresh copy of its base image. All data on the disk is permanently erased. The VM must be stopped. Any existing snapshot is cleared.

Destructive operation. All data on the VM's disk is lost. This cannot be undone.

FieldDescription
ssh_public_keySSH public key to inject into the fresh disk. Required if you want SSH access after rebuild (the original key is not preserved).
curl -X POST https://api.betawings.com/api/v1/vms/3f9a1c2b4e5d6f7a/rebuild \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"ssh_public_key": "ssh-ed25519 AAAA... you@host"}'

Migrate VM

POST /api/v1/vms/{id}/migrate

Moves a VM's disk to a different node. The VM's rootfs.ext4 is streamed from the source host through the CP to the target host. After migration the VM can be started on the new host with a cold boot.

The VM must be stopped before migrating. The operation is crash-safe: the DB node_id is only updated after the target has received the full rootfs.

Firecracker snapshots are not transferred — they are CPU-architecture-specific and cannot be restored cross-host. Snapshot state is cleared as part of the migration; the first start on the new host will always be a cold boot.

FieldDescription
target_node_idrequired Destination node ID, or "local" to move the VM to the control-plane host.
# Move a local VM onto a remote node
curl -X POST https://api.betawings.com/api/v1/vms/3f9a1c2b4e5d6f7a/migrate \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"target_node_id": "8d82890e3c0fb5aa"}'
# Pull a VM from a remote node back to local
curl -X POST https://api.betawings.com/api/v1/vms/3f9a1c2b4e5d6f7a/migrate \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"target_node_id": "local"}'

Destroy VM

DELETE /api/v1/vms/{id}

Permanently deletes a VM — the Firecracker process, disk image, snapshots, and DB record are all removed. This cannot be undone.

Returns 204 No Content on success.

curl -X DELETE https://api.betawings.com/api/v1/vms/3f9a1c2b4e5d6f7a \
  -H "Authorization: Bearer $TOKEN"

Set Idle Config

POST /api/v1/vms/{id}/idle-config

Updates the VM's idle timeout. When the VM has had no activity for this many seconds, the idle checker automatically creates a Firecracker memory snapshot and stops the VM — freeing its CPU and RAM. The next incoming HTTPS, SSH, or HTTP request to the VM's domain triggers an automatic resume from snapshot (~300 ms).

What counts as activity: any proxied connection through the Betawings gateway (HTTPS, SSH, HTTP), or an explicit Report Activity call from inside the VM. The timer resets on each event.

FieldDescription
idle_timeout_secondsrequired Integer 10–86400 (10 seconds to 24 hours). Default is 300 (5 minutes).
curl -X POST https://api.betawings.com/api/v1/vms/3f9a1c2b4e5d6f7a/idle-config \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"idle_timeout_seconds": 1800}'

Get Activity Info

GET /api/v1/vms/{id}/activity

Returns how long the VM has been idle and when it will be shut down by the idle checker.

curl https://api.betawings.com/api/v1/vms/3f9a1c2b4e5d6f7a/activity \
  -H "Authorization: Bearer $TOKEN"
{
  "last_activity_at":      "2026-07-11T10:05:00Z",
  "idle_for_seconds":      120,
  "will_shutdown_in_seconds": 180
}

Report Activity

POST /api/v1/vms/{id}/activity

Resets the idle timer, signalling that the VM is still in use. Call this from inside the VM (or from a client) when work is happening, to prevent an idle shutdown. The source field is informational only.

curl -X POST https://api.betawings.com/api/v1/vms/3f9a1c2b4e5d6f7a/activity \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"source": "user-request", "packet_count": 42}'

Get VM Logs

GET /api/v1/vms/{id}/logs

Returns the last N lines of the VM's Firecracker log. The log contains kernel boot messages, device initialisation output, and any text written by the guest to the serial console. Useful for diagnosing boot failures and startup scripts.

Query paramDescription
linesNumber of lines to return. Default 100, max 10000.
curl "https://api.betawings.com/api/v1/vms/3f9a1c2b4e5d6f7a/logs?lines=200" \
  -H "Authorization: Bearer $TOKEN"

Logs are available whether the VM is running or stopped. They persist across restarts and snapshots for the lifetime of the VM.

SSH Access

Every VM created with an ssh_public_key is reachable via the Betawings SSH proxy at api.betawings.com:2222. The connection is forwarded byte-for-byte to your VM — your private key never touches the gateway.

Overview

Betawings uses SSH ProxyJump: your SSH client connects to the gateway at api.betawings.com:2222, which resolves the target VM by name and forwards the connection directly. The gateway never terminates or inspects the SSH session.

The SSH key you pass when creating the VM is injected into /root/.ssh/authorized_keys on the guest disk. The gateway stores only the SHA-256 fingerprint for routing — it never holds the private or public key material for authentication purposes.

Connect directly without a config file using the -J ProxyJump flag:

ssh -J root@api.betawings.com:2222 root@my-project.onbw.net

SSH Config

Add the following block to ~/.ssh/config for seamless access to all your VMs:

# Wings gateway
Host wings-gw
  HostName api.betawings.com
  Port 2222
  User root
  IdentityFile ~/.ssh/your-key

# All VMs on *.onbw.net jump through the gateway
Host *.onbw.net
  User root
  ProxyJump wings-gw
  IdentityFile ~/.ssh/your-key

Once configured, connect with just:

ssh my-project.onbw.net

The hostname my-project must match the name field of your VM exactly.

Wake on SSH

If the VM is currently sleeping (stopped with a snapshot), the SSH proxy automatically wakes it before forwarding the connection. Your SSH client holds the TCP connection open while the VM resumes from snapshot — typically under 300 ms. No retry is needed on the client side.

An SSH connection to a sleeping VM resets the idle timer as soon as the VM wakes. The VM will not be shut down again until it has been idle for the full idle_timeout_seconds after the session ends.

The VM must have been created with ssh_public_key. If no key was provided at creation time, SSH access is not available without a Rebuild.

Custom Domains

By default every VM is reachable at name.onbw.net. Custom domains let you route your own domain to a VM with ownership verified via a DNS TXT record.

The typical flow is: Add domain → create the TXT record in your DNS provider → Verify → point your domain's A/AAAA record to 51.159.66.179. Wings will begin routing HTTPS traffic to the VM as soon as DNS propagates. TLS is end-to-end: Wings reads only the SNI hostname from your HTTPS connections and does not terminate or inspect the TLS session.

List Domains

GET /api/v1/vms/{id}/domains
curl https://api.betawings.com/api/v1/vms/3f9a1c2b4e5d6f7a/domains \
  -H "Authorization: Bearer $TOKEN"

Add Domain

PUT /api/v1/vms/{id}/domains/{domain}

Registers a custom domain for a VM and returns a DNS challenge value. The domain is not yet verified — create the TXT record and call Verify.

curl -X PUT https://api.betawings.com/api/v1/vms/3f9a1c2b4e5d6f7a/domains/myapp.example.com \
  -H "Authorization: Bearer $TOKEN"
{
  "domain":      "myapp.example.com",
  "vm_id":       "3f9a1c2b4e5d6f7a",
  "status":      "pending_verification",
  "txt_record":  "_wings-challenge.myapp.example.com",
  "txt_value":   "wings-a1b2c3d4...",
  "verify_hint": "Set TXT record, then POST /api/v1/vms/.../domains/.../verify"
}

Verify Domain

POST /api/v1/vms/{id}/domains/{domain}/verify

Checks that the DNS TXT record _wings-challenge.{domain} contains the challenge value issued by Add Domain. On success the domain is marked verified and Wings begins routing it to the VM.

curl -X POST https://api.betawings.com/api/v1/vms/3f9a1c2b4e5d6f7a/domains/myapp.example.com/verify \
  -H "Authorization: Bearer $TOKEN"

Remove Domain

DELETE /api/v1/vms/{id}/domains/{domain}

Removes the custom domain association. Wings stops routing that domain to the VM. Returns 204 No Content.

curl -X DELETE https://api.betawings.com/api/v1/vms/3f9a1c2b4e5d6f7a/domains/myapp.example.com \
  -H "Authorization: Bearer $TOKEN"

API Tokens

Create Token

POST /api/v1/tokens/

Creates a new API token. The raw token value is returned once — it is not stored and cannot be retrieved later. Store it securely immediately.

FieldDescription
namerequired Human-readable label for this token.
rolerequired One of: org-admin, creator, reader. (Platform-level roles are only assignable by a cloud-admin.)
org_idRequired for org-scoped roles (org-admin, creator, reader).
expires_atOptional RFC 3339 expiry timestamp. Token is rejected after this time.
curl -X POST https://api.betawings.com/api/v1/tokens/ \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "ci-pipeline",
    "role": "creator",
    "org_id": "a1b2c3d4e5f6",
    "expires_at": "2027-01-01T00:00:00Z"
  }'
{
  "token":      "bwt_a1b2c3...<only shown once>",
  "id":         "7c8d9e0f1a2b",
  "name":       "ci-pipeline",
  "role":       "creator",
  "org_id":     "a1b2c3d4e5f6",
  "scope_type": "org",
  "created_at": "2026-07-11T10:00:00Z"
}

List Tokens

GET /api/v1/tokens/

Returns all tokens visible to the caller. Cloud-admin sees all tokens; org-scoped callers see only tokens belonging to their org. Raw token values are never returned.

curl https://api.betawings.com/api/v1/tokens/ \
  -H "Authorization: Bearer $TOKEN"

Delete Token

DELETE /api/v1/tokens/{id}

Revokes a token immediately. All subsequent requests using that token receive 401 Unauthorized. Returns 204 No Content.

curl -X DELETE https://api.betawings.com/api/v1/tokens/7c8d9e0f1a2b \
  -H "Authorization: Bearer $TOKEN"

Best practices: use the most restrictive role that covers your use case (prefer creator or reader over org-admin for automated pipelines). Set expires_at on all CI/CD tokens. Rotate tokens that may have been exposed — deletion is instant.

Error Reference

All errors return a JSON body with an error field describing what went wrong. Common HTTP status codes:

StatusMeaningCommon cause
400Bad RequestMissing required field, invalid value (e.g. name contains uppercase), or constraint violation (e.g. resizing a running VM).
401UnauthorizedMissing or invalid bearer token. Token may have expired or been revoked.
403ForbiddenToken is valid but lacks permission for this action (e.g. reader attempting to create a VM).
404Not FoundVM, domain, or token with that ID does not exist, or belongs to a different org.
409ConflictName already in use (VM or domain). Domain ownership already claimed by another VM.
429Too Many RequestsVM creation rate limit exceeded. Back off and retry.
500Internal ErrorUnexpected server error. Check VM logs; if persistent, contact support.

System

Health Check

GET /health

No authentication required. Returns platform health and VM counts. Use this for uptime monitoring.

curl https://api.betawings.com/health
{
  "status":      "ok",
  "vms_running": 4,
  "vms_stopped": 12,
  "vms_total":   16
}

List Images

GET /api/v1/images

Returns available base disk images. The name field is what you pass as image when creating a VM.

curl https://api.betawings.com/api/v1/images \
  -H "Authorization: Bearer $TOKEN"
[{
  "name":       "demo-44322",
  "size_bytes": 4294967296
}]