API reference
August 31, 2026 · View on GitHub
This document reflects the Rust handlers in crates/taritd/src/api.rs and crates/taritd/src/internal.rs. The served openapi.yaml covers all public routes; it does not describe the internal peer routes, and this document remains the more detailed reference for behavior and status codes.
Authentication
| Surface | Header | Applies to |
|---|---|---|
| Public API | X-API-Key: <api key> | All /v1/* routes. Keys resolve to a tenant and role. |
| PTY WebSocket | ?token=<connect_token> | /v1/vms/{id}/pty/{pty_id}/connect. The session token comes from the create-session response and expires after 5 minutes without a connection. |
| Peer API | target-bound request HMAC derived from TARIT_PEER_SECRET | All /internal/v1/* routes. The shared key is never sent. |
| Unauthenticated | none | /health, /livez, /startupz, /readyz, /openapi.yaml, /docs. |
Error responses from public handlers use:
{ "error": "message" }
The peer HMAC middleware returns a bare 401 Unauthorized with no JSON body.
API keys are configured either with legacy TARIT_API_KEY (tenant default,
role admin, unlimited VMs), TARIT_API_KEYS="key:tenant:role:max_vms,...",
or [api_keys] in TARIT_CONFIG:
[api_keys]
"key1" = { tenant = "tenantA", role = "user", max_vms = 20 }
"key2" = { tenant = "tenantB", role = "admin", max_vms = 0 }
Roles are admin or user; max_vms = 0 means unlimited. Raw keys are hashed
in memory before comparison. User keys only see and act on their tenant's VMs;
admin keys can call admin-only routes such as /v1/cluster.
Common data types
VmRecord
{
"id": "uuid",
"status": "creating|running|paused|suspended|hibernated|stopped|error",
"revision": 3,
"startup_path": "cold|warm|snapshot_restore",
"memory_mib": 256,
"vcpus": 1,
"created_at": "2026-07-02T00:00:00Z",
"updated_at": "2026-07-02T00:00:01Z"
}
This public representation deliberately omits the physical host id, tenant ownership metadata, kernel/rootfs/socket paths, kernel command line, and VMM process id. Those fields exist only in node persistence and authenticated peer RPC. Successfully deleted records are not returned by list or get.
LiveVmStatus
Returned by the public status endpoint after sanitizing the owning VMM's live
status. It is not the stored record returned by GET /v1/vms/{id}.
{
"state": "created|running|paused|suspended|stopped",
"uptime_ms": 1037,
"vcpus": 1,
"mem_mib": 256,
"vcpu_alive": true
}
ExecutionRecord
{
"id": "uuid",
"vm_id": "uuid",
"command": "echo hello",
"timeout_ms": 30000,
"status": "pending|running|completed|failed",
"exit_code": 0,
"stdout": "hello\n",
"stderr": "",
"duration_ms": 42,
"error": null,
"created_at": "2026-07-02T00:00:00Z",
"updated_at": "2026-07-02T00:00:01Z"
}
When pending or running, result fields are usually null.
Public endpoints
GET /health
No authentication. Used by load balancers.
Response 200:
{ "status": "ok" }
GET /metrics
Requires an admin API key. Returns Prometheus text exposition metrics for the
local taritd process. Includes taritd_tenant_vms{tenant="..."} for local
active VM counts by tenant, plus bounded PTY metrics for active connections,
configured global/tenant/VM limits, and admission rejections by scope. The
tenant and per-VM vm_id labels are a stable short hash by default so scraping
cannot enumerate tenant or VM identities; set
TARIT_METRICS_EXPOSE_TENANT_LABELS=1 to expose raw values on a trusted network.
PTY samples are taritd_pty_active_connections,
taritd_pty_connection_limit{scope="..."}, and
taritd_pty_admission_rejections_total{scope="..."}; their scope label is one
of global, tenant, or vm and never contains an identity.
Response 200: text/plain; version=0.0.4.
GET /openapi.yaml
No authentication. Returns the bundled OpenAPI YAML with http://localhost:8080 rewritten from the request Host header.
Response 200: application/yaml.
GET /docs
No authentication. Returns Swagger UI HTML that loads /openapi.yaml.
Response 200: text/html.
Port shares
Port-share control routes use X-API-Key and are separate from the guest
gateway. The gateway listener requires TARIT_SHARE_LISTEN,
TARIT_SHARE_DOMAIN, and TARIT_SHARE_TOKEN_KEY; it accepts requests for
<slug>.<TARIT_SHARE_DOMAIN> and is not an API-key route. Disabling only
TARIT_SHARE_LISTEN leaves the control routes below available on the normal
API listener. See CONFIGURATION.md and
deploy/Caddyfile.shares.example.
ShareRecord:
{
"id": "uuid",
"slug": "lowercase-dns-label",
"owner_key": "tenant-a",
"vm_id": "uuid",
"guest_port": 8080,
"visibility": "private",
"token_version": 0,
"revoked_at": null,
"created_at": "2026-07-12T00:00:00Z",
"updated_at": "2026-07-12T00:00:00Z"
}
Visibility is private or public; omitted create visibility defaults to
private, while omitted update visibility preserves the existing value.
guest_port must be in 1..=65535. Create and update bodies reject unknown
fields.
| Method | Path | Success | Behavior |
|---|---|---|---|
POST | /v1/shares | 201 ShareRecord | Create a share for a running VM the caller can access. |
GET | /v1/shares | 200 ShareRecord[] | List shares owned by the caller's tenant. |
GET | /v1/shares/{id} | 200 ShareRecord | Get a share owned by the caller's tenant, or any share with an admin API key. |
PATCH | /v1/shares/{id} | 200 ShareRecord | Update the VM, guest port, and/or visibility. |
DELETE | /v1/shares/{id} | 204 | Revoke a share. |
POST | /v1/shares/{id}/tokens | 200 ShareTokenResponse | Issue a private-share gateway token. |
User API keys can access only their tenant's existing shares. An admin API
key can get, update, revoke, or issue a token for an existing share in any
tenant; listing remains scoped to the caller's tenant.
Create request:
{
"vm_id": "uuid",
"guest_port": 8080,
"visibility": "private"
}
Update request fields are all optional:
{
"guest_port": 3000,
"visibility": "public"
}
The replacement vm_id, when supplied, must identify a running VM accessible
to the caller and owned by the same tenant as the share. A revoked share cannot
be updated. Changing vm_id, guest_port, or visibility increments
token_version and invalidates all previously issued private-share tokens.
Revocation also increments token_version, sets revoked_at, removes the
share from the gateway, and is idempotent for an authorized caller.
Only active private shares can issue tokens:
{
"token": "base64url-payload.base64url-signature",
"expires_at": "2026-07-12T00:05:00Z"
}
Use the token only in the guest request header:
X-Tarit-Share-Token: <token>
The gateway rejects absent, malformed, duplicate, expired, or
token-version-mismatched private tokens with 401; revoked shares return
404 before token verification. Tokens expire at
expires_at, which is issuance time plus TARIT_SHARE_TOKEN_TTL_SECS. Public
shares do not need a token; share tokens are not accepted in query parameters.
For POST /v1/shares/{id}/tokens, 400 means an invalid identifier or a
request for a public or revoked share; an unknown share is 404.
Share control error bodies are JSON: { "error": "..." }. Across the six
control operations, 400 means an invalid identifier or request body (plus
the public/revoked token cases above), 401
means a missing or invalid API key, 403 means another tenant's share or VM,
404 means an unknown share or VM, 409 means a revoked share, non-running VM,
or mutation conflict, and 503 means the share owner, share service, or audit
service is unavailable. GET /v1/shares can return 200, 401, or 503;
other statuses apply only where their operation can produce that condition.
POST /v1/vms
Create a VM owned by the caller's tenant. The receiving node tries local warm or cold capacity first, then all healthy peers with capacity. If the tenant is at its configured VM quota, the response is 403 Forbidden. If the whole visible cluster remains full for the admission window, it returns 429 Too Many Requests with Retry-After: <seconds>.
Request:
{
"id": "optional uuid",
"memory_mib": 256,
"vcpus": 1,
"kernel_path": "optional admin-only kernel override",
"image": "optional registered image name[:tag]",
"rootfs_path": "optional admin-only rootfs override, empty string means no rootfs",
"cmdline": "optional kernel command line override"
}
Defaults from tarit-types and Config:
| Field | Default |
|---|---|
id | generated UUID |
memory_mib | 256 |
vcpus | 1 |
kernel_path | TARIT_KERNEL; admin-only override |
image | unset; when present it resolves through the node-local image registry and cannot be combined with rootfs_path |
rootfs_path | TARIT_ROOTFS; admin-only override; empty string disables rootfs |
cmdline | default virtio block kernel cmdline when rootfs exists, otherwise console=ttyS0 panic=1 |
Response 201: VmRecord.
Status codes:
| Status | Meaning |
|---|---|
201 | VM created locally or on a peer. |
400 | Malformed request body or other bad request. |
401 | Missing or wrong X-API-Key. |
403 | Tenant VM quota reached. |
409 | Requested VM id already exists or another genuine state conflict occurred. |
429 | Cluster stayed at capacity until TARIT_ADMISSION_TIMEOUT_MS; includes Retry-After in seconds. |
500 | VMM, peer, fleet, or internal failure. |
GET /v1/vms
List VM records visible to the caller. User keys only see their tenant's VMs; admin keys can see all local VM records.
Response 200:
[
{ "id": "uuid", "host_id": "node-a", "status": "running" }
]
The actual objects are full VmRecord values. In cluster mode this endpoint is not a cluster-wide list. It does not aggregate peer stores or read fleet_vms.
Status codes: 200, 401, 500.
GET /v1/vms/{id}
Resolve owner through the fleet registry and return the VM record from the owner.
User keys can only read VMs owned by their tenant; otherwise the response is
403 Forbidden. Admin keys can read any tenant's VM.
Response 200: VmRecord.
Status codes:
| Status | Meaning |
|---|---|
200 | VM found. |
401 | Missing or wrong X-API-Key. |
403 | VM belongs to a different tenant. |
404 | VM not found in fleet or local fallback. |
500 | Internal store or protocol failure. Details are not exposed. |
503 | The owning host is unhealthy, stale, or unavailable. |
GET /v1/vms/{id}/status
Resolve owner through the fleet registry and query the owning VMM over its Unix socket for live status.
Response 200: LiveVmStatus.
This differs from GET /v1/vms/{id}: /status reports sanitized live state,
uptime, shape, and vcpu_alive; it does not expose the VMM's kernel path or
device configuration.
Status codes: 200, 401, 403, 404, 409 (VM is stopped), 500, 503.
SSH keys
All SSH key records are scoped to the caller's tenant. RSA keys remain valid for guest authorized-key injection but cannot authenticate to the SSH gateway.
POST /v1/ssh-keys
Request:
{ "public_key": "ssh-ed25519 AAAA... comment" }
Response 201:
{
"id": "uuid",
"fingerprint": "SHA256:...",
"key_type": "ssh-ed25519",
"created_at": "2026-07-02T00:00:00Z"
}
GET /v1/ssh-keys response 200:
{ "keys": [] }
DELETE /v1/ssh-keys/{key_id} response 204: no body.
Status codes: 200, 201, 204, 400, 401, 404, 500.
PTY sessions and WebSocket attach
PTY routes operate on the VM's owning node. If a request lands on a non-owner, these routes return 409; connect to the owner node or use load-balancer stickiness for PTY sessions.
POST /v1/vms/{id}/pty/sessions
Request:
{ "cols": 80, "rows": 24, "shell": "/bin/bash" }
Response 201:
{ "pty_id": "uuid", "cols": 80, "rows": 24, "connect_token": "..." }
connect_token is a per-session secret. Pass it as the token query parameter
when attaching over the WebSocket route below. It expires five minutes after
creation or the most recent disconnect.
Additional REST routes:
| Method | Path | Response |
|---|---|---|
GET | /v1/vms/{id}/pty/sessions | { "sessions": [...] } |
GET | /v1/vms/{id}/pty/sessions/{pty_id} | PTY session record |
DELETE | /v1/vms/{id}/pty/sessions/{pty_id} | 204 no body |
POST | /v1/vms/{id}/pty/sessions/{pty_id}/resize | { "pty_id": "uuid", "cols": N, "rows": N } |
WS /v1/vms/{id}/pty/{pty_id}/connect?token=<connect_token> upgrades to a WebSocket. It authenticates with the session's connect_token, not the API key. One connection may be active for a session at a time; after disconnect, the same session may reconnect for another five minutes. Before upgrading, taritd reserves the configured global, authenticated-tenant, and VM active-connection capacity. Capacity rejection returns HTTP 429 without invalidating the token. Binary messages are raw PTY bytes. Text messages are JSON controls: client-to-server {"type":"resize","cols":N,"rows":N} and server-to-client {"type":"exit","exit_code":N}. VM teardown removes all of its pending and connected session records.
Status codes: 200, 201, 204, 400, 401, 404, 409, 429, 500. A bad, missing, expired, or unknown connect token is rejected with HTTP 401 before upgrade. A second concurrent connection or deletion of a connected session returns 409. Post-upgrade failures close the socket with 1013 when the VM is unavailable on this node and 1011 for attach or stream errors.
DELETE /v1/vms/{id}
Resolve owner, stop the VM on its owner, mark the owner's local record as stopped, release the local scheduler slot, and remove the VM ownership row from fleet_vms.
Response 204: no body.
Status codes: 204, 401, 403, 404, 500.
Note: the local SQLite VM row is not deleted. On the owner, a later local GET can still find the stopped record. Other nodes generally cannot find it after fleet_vms is cleared.
POST /v1/vms/{id}/pause
Resolve the owner, stop every vCPU, drain and park the block, network, and vsock workers, then publish the paused state. The public handler does not require a JSON body.
If a vCPU or device-worker transition fails and the VMM cannot confirm a safe rollback, it fences the VM paused. The orchestrator observes and durably records that state before returning the operation failure, allowing a later explicit resume or delete instead of leaving the control plane marked running.
Response 200: updated VmRecord with status: "paused".
Status codes: 200, 401, 403, 404, 409 (invalid lifecycle
transition), 500.
POST /v1/vms/{id}/suspend
Resolve the owner and capture a coherent in-process suspend image after every vCPU and guest-memory-writing device worker acknowledges quiescence. Resident guest RAM is released, but VM ownership, the VMM process, scheduler capacity, and tenant quota remain reserved.
Response 200: updated VmRecord with status: "suspended".
Status codes: 200, 401, 403, 404, 409 (invalid lifecycle
transition), 500.
POST /v1/vms/{id}/hibernate
Capture and authenticate a live RAM, device, and private-disk artifact, satisfy the configured replication policy, stop the resident VMM, and release CPU, memory, cgroup, network, and scheduler capacity. The logical VM and its tenant ownership remain durable for later activation.
Response 200: updated VmRecord with status: "hibernated".
Status codes: 200, 401, 403, 404, 409 (VM is not running), 500,
503 (durable artifact or peer lifecycle requirements are unavailable).
POST /v1/vms/{id}/resume
Resolve the owner and resume a paused or suspended VM. A hibernated VM is activated through the fenced single-flight restore path, including normal placement, artifact verification, network repair, and policy restoration. Device workers must leave their parked state before vCPUs restart, and the operation returns only after guest readiness succeeds. The public handler does not require a JSON body.
Response 200: updated VmRecord with status: "running".
Status codes: 200, 401, 403, 404, 409 (invalid lifecycle state),
429 (no placement capacity), 500, 503 (owner or restore prerequisites are
unavailable).
POST /v1/vms/{id}/snapshot
Resolve the owner and publish an authenticated snapshot behind an opaque UUID.
Host paths, physical host identity, and artifact locators remain private. Only
full snapshots are accepted. diff: true returns 422 before contacting the
VMM because durable parent-chain relocation is not implemented yet.
Request:
{ "diff": false }
diff defaults to false.
Response 200:
{
"snapshot_id": "uuid"
}
Status codes: 200, 401, 403, 404, 409 (the lifecycle state does not
support snapshots), 422 (diff is true), 500.
POST /v1/restore
Restore a VM from an opaque tenant-owned snapshot handle. The control plane resolves its private locator, verifies the authenticated artifact, and routes the restore to the node that holds it. Clients cannot provide a host, path, or storage locator.
Request:
{
"snapshot_id": "uuid",
"id": "optional new vm uuid"
}
Response 201: VmRecord.
Status codes:
| Status | Meaning |
|---|---|
201 | VM restored and ready. |
401 | Missing or wrong X-API-Key. |
403 | Tenant VM quota is reached. |
404 | Snapshot handle not found or belongs to another tenant. |
409 | Requested VM id already exists. |
429 | The snapshot-owning node has no capacity; includes Retry-After in seconds. |
500 | Internal restore failure. |
503 | The snapshot-owning node is unhealthy, stale, or unavailable. |
POST /v1/execute
Resolve the VM owner and run a command synchronously: one request returns the finished execution record, no polling. Use this for low-latency request/response exec; use POST /v1/execute_async plus GET /v1/executions/{id} when you would rather poll.
Request:
{
"vm_id": "uuid",
"command": "echo hello",
"timeout_ms": 30000
}
timeout_ms defaults to 30000.
Response 200: final ExecutionRecord with status: "completed" (result fields exit_code, stdout, stderr, duration_ms set) or status: "failed" (error set). A command that runs but exits non-zero is still completed; check exit_code. An exec that could not run at all (for example, guest agent unavailable) returns 200 with status: "failed", not an HTTP error.
Status codes:
| Status | Meaning |
|---|---|
200 | Execution finished; the record carries the outcome (completed or failed). |
401 | Missing or wrong X-API-Key. |
403 | VM belongs to a different tenant. |
404 | VM not found. |
500 | Owner resolution, peer, or internal failure. |
POST /v1/execute_async
Resolve the VM owner and run a command asynchronously. The execution record is created on the API node that accepts the request, even when the VM owner is remote.
Request:
{
"vm_id": "uuid",
"command": "echo hello",
"timeout_ms": 30000
}
timeout_ms defaults to 30000.
Response 202: initial ExecutionRecord with status: "pending".
Status codes:
| Status | Meaning |
|---|---|
202 | Execution accepted. |
401 | Missing or wrong X-API-Key. |
403 | VM belongs to a different tenant. |
404 | VM not found. |
500 | Store, peer, or VMM failure. |
Polling note: GET /v1/executions/{id} must hit the same API node that accepted the execution request unless an external system replicates execution records. Execution status lookups are not routed through the fleet.
GET /v1/executions/{id}
Return an execution record from the receiving node's local SQLite store.
Response 200: ExecutionRecord.
Status codes: 200, 401, 403, 404, 500.
PATCH /v1/egress/vm/{id}
Resolve owner and update the running VM's egress allowlist through the VMM.
Request:
{
"allowlist": ["10.0.0.0/8:443/tcp"],
"allow_existing": true
}
allow_existing defaults to false.
Response 200:
{ "rules_applied": 1 }
Status codes: 200, 401, 403, 404, 409 (VM is stopped), 500.
GET /v1/cluster
Admin-only. Return cluster capacity and health. In cluster mode, data comes from PostgreSQL fleet_hosts. In single-host mode, data comes from the local SQLite host roster.
Response 200:
{
"this_host": "node-a",
"clustered": true,
"total_nodes": 3,
"healthy_nodes": 2,
"cluster_free_vcpus": 10,
"cluster_free_memory_mib": 24576,
"nodes": [
{
"host_id": "node-a",
"rpc_addr": "http://10.0.1.10:8080",
"sandbox_count": 4,
"free_vcpus": 12,
"free_memory_mib": 32768,
"up": true,
"last_heartbeat": "2026-07-02T00:00:00Z"
}
]
}
healthy_nodes, cluster_free_vcpus, and cluster_free_memory_mib include only nodes with healthy = true and a heartbeat fresher than about 15 seconds.
Status codes: 200, 401, 403, 500.
GET /v1/usage
Aggregated per-key usage stats from the primary store. Requires a fleet database. Admins see every key; a non-admin key sees only its own. Query parameters: from, to (RFC3339; default last 30 days), and api_key_id (admin only).
[
{ "api_key_id": "a9842b8c...c385aa", "owner_key": "default", "vm_runtime_seconds": 11.05, "exec_count": 1, "exec_duration_ms": 1 }
]
Status codes: 200, 401, 500 (500 if no fleet database is configured).
GET /v1/audit
Recent audit trail, newest first. Requires a fleet database. Admins see every key; a non-admin key sees only its own. Query parameters: api_key_id (admin only), vm_id, limit (default 100, max 1000).
[
{ "id": "…", "api_key_id": "a9842b8c...c385aa", "owner_key": "default", "host_id": "node-a", "vm_id": "33827851-…", "action": "exec", "outcome": "ok", "detail": null, "created_at": "2026-07-03T00:00:00Z" }
]
Both endpoints record raw stats only. See USAGE-AND-AUDIT.md.
Status codes: 200, 401, 500.
Internal peer API
Internal routes are currently mounted on the same HTTP listener. Each request
uses a short-lived HMAC bound to method, path, body hash, nonce, source host, and
target host; caller identity is signed separately and replay protected. The
shared key is never transmitted, and the legacy X-Peer-Secret header is
rejected. This is not a substitute for a separate internal listener with mTLS
and host-session fencing; keep /internal/v1/* unreachable from public
networks.
Internal endpoint table
| Method | Path | Body | Success | Purpose |
|---|---|---|---|---|
| POST | /internal/v1/vms | CreateVmRequest | 201 VmRecord | Create on this node only. Returns 429 + Retry-After if this node is full. |
| POST | /internal/v1/restore | RestoreRequest | 201 VmRecord | Restore on this node only. |
| GET | /internal/v1/vms/{id} | none | 200 VmRecord | Get from this node's local store. |
| GET | /internal/v1/vms/{id}/status | none | 200 LiveVmStatus | Query live status from this node's local VMM. |
| DELETE | /internal/v1/vms/{id} | none | 204 | Stop on this node and clear local/fleet ownership. |
| POST | /internal/v1/vms/{id}/exec | {"command":"...","timeout_ms":30000} | 200 {exit_code,stdout,stderr,duration_ms} | Execute on this node's VM. |
| POST | /internal/v1/vms/{id}/pause | none | 200 VmRecord | Pause local VM. |
| POST | /internal/v1/vms/{id}/resume | none | 200 VmRecord | Resume local VM. |
| POST | /internal/v1/vms/{id}/snapshot | {"diff":false} | 200 {path,host_id} | Snapshot local VM. |
| PATCH | /internal/v1/vms/{id}/egress | EgressUpdateRequest | 200 {rules_applied} | Update local VM egress. |
Internal handlers call the same ops::*_local functions as public handlers, so local behavior is shared. Peer-forwarded public requests carry the resolved caller identity in the X-Tarit-Tenant, X-Tarit-Role, and X-Tarit-Api-Key-Id headers so the owner node can enforce tenant access before local VM operations. Requests without a valid tenant and role are rejected with 401; X-Tarit-Api-Key-Id may be empty. They intentionally do not perform owner resolution.
Status code summary
| Status | Meaning |
|---|---|
200 | Successful read, pause, resume, snapshot, egress update, synchronous execute, or cluster status. |
201 | Successful create or restore. |
202 | Execution accepted. |
204 | Successful delete/stop. |
400 | Bad request. Usually malformed JSON or invalid payload shape. |
401 | Missing or invalid API key or peer secret. |
403 | Authenticated caller is not allowed to access the tenant resource, lacks admin role, or exceeded tenant VM quota. |
404 | VM, execution, owner host, or snapshot owner not found. |
409 | Genuine state conflict, such as a duplicate requested VM id or an operation invalid for the current VM state. |
429 | Capacity or overload backpressure. Create/restore responses include Retry-After in seconds. |
500 | Internal, VMM, peer, store, or fleet failure. |