boxd.sh:9443. If your language doesn’t have a boxd SDK yet, generate one from the proto file and talk to it directly.
Endpoint
boxd.sh:9443
- Transport: HTTP/2 cleartext (h2c). No TLS on this port — auth is short-lived JWTs.
- Reflection: gRPC server reflection is enabled, so tools like
grpcurlwork without the proto file. - Service:
boxd.api.v1.BoxdApi
The proto
Copy the full service definition into a localapi.proto, then generate stubs with protoc, buf, grpc_tools, or @grpc/proto-loader. The proto is proto3 with no external imports — generation is one command.
You don’t strictly need the file: since server reflection is on, grpcurl and buf curl work directly against the endpoint, and grpcurl -plaintext boxd.sh:9443 describe boxd.api.v1.BoxdApi dumps the schema on demand.
Show api.proto — full service definition
Show api.proto — full service definition
syntax = "proto3";
package boxd.api.v1;
service BoxdApi {
rpc CreateVm(CreateVmRequest) returns (CreateVmResponse);
rpc DestroyVm(DestroyVmRequest) returns (DestroyVmResponse);
rpc StartVm(StartVmRequest) returns (StartVmResponse);
rpc StopVm(StopVmRequest) returns (StopVmResponse);
rpc RebootVm(RebootVmRequest) returns (RebootVmResponse);
rpc GetVm(GetVmRequest) returns (GetVmResponse);
rpc ListVms(ListVmsRequest) returns (ListVmsResponse);
rpc ExposePort(ExposePortRequest) returns (ExposePortResponse);
rpc UnexposePort(UnexposePortRequest) returns (UnexposePortResponse);
rpc ListExposedPorts(ListExposedPortsRequest) returns (ListExposedPortsResponse);
rpc StreamLogs(StreamLogsRequest) returns (stream LogChunk);
rpc Exec(stream ExecChunk) returns (stream ExecChunk);
rpc CreateNetwork(CreateNetworkRequest) returns (CreateNetworkResponse);
rpc ListNetworks(ListNetworksRequest) returns (ListNetworksResponse);
rpc BindDomain(BindDomainRequest) returns (BindDomainResponse);
rpc UnbindDomain(UnbindDomainRequest) returns (UnbindDomainResponse);
rpc ListDomains(ListDomainsRequest) returns (ListDomainsResponse);
rpc Whoami(WhoamiRequest) returns (WhoamiResponse);
rpc CreateToken(CreateTokenRequest) returns (CreateTokenResponse);
rpc ListTokens(ListTokensRequest) returns (ListTokensResponse);
rpc RevokeToken(RevokeTokenRequest) returns (RevokeTokenResponse);
rpc GetConfig(GetConfigRequest) returns (GetConfigResponse);
rpc ForkVm(ForkVmRequest) returns (ForkVmResponse);
rpc ListProxies(ListProxiesRequest) returns (ListProxiesResponse);
rpc CreateProxy(CreateProxyRequest) returns (CreateProxyResponse);
rpc DeleteProxy(DeleteProxyRequest) returns (DeleteProxyResponse);
rpc SetProxyPort(SetProxyPortRequest) returns (SetProxyPortResponse);
rpc UploadFile(UploadFileRequest) returns (UploadFileResponse);
// Streaming upload: lifts the 4 MiB single-message gRPC cap of UploadFile.
// The first chunk carries vm_id/path/total_size; subsequent chunks carry
// bytes only. Server side reads exactly total_size bytes via `head -c N`,
// verifies the file size, and returns the confirmed byte count.
rpc UploadFileStream(stream UploadFileChunk) returns (UploadFileResponse);
rpc DownloadFile(DownloadFileRequest) returns (DownloadFileResponse);
rpc SuspendVm(SuspendVmRequest) returns (SuspendVmResponse);
rpc ResumeVm(ResumeVmRequest) returns (ResumeVmResponse);
rpc SetAutoSuspendTimeout(SetAutoSuspendTimeoutRequest) returns (SetAutoSuspendTimeoutResponse);
rpc SetAutoHibernateTimeout(SetAutoHibernateTimeoutRequest) returns (SetAutoHibernateTimeoutResponse);
// Disks
rpc CreateDisk(CreateDiskRequest) returns (CreateDiskResponse);
rpc ListDisks(ListDisksRequest) returns (ListDisksResponse);
rpc AttachDisk(AttachDiskRequest) returns (AttachDiskResponse);
rpc DetachDisk(DetachDiskRequest) returns (DetachDiskResponse);
rpc DestroyDisk(DestroyDiskRequest) returns (DestroyDiskResponse);
// API Keys
rpc CreateApiKey(CreateApiKeyRequest) returns (CreateApiKeyResponse);
rpc ListApiKeys(ListApiKeysRequest) returns (ListApiKeysResponse);
rpc DeleteApiKey(DeleteApiKeyRequest) returns (DeleteApiKeyResponse);
// Billing — individual subscriptions. ListPlans is unauth-friendly
// (returns the static four-shape catalog), the rest require JWT.
rpc ListPlans(ListPlansRequest) returns (ListPlansResponse);
rpc GetBilling(GetBillingRequest) returns (GetBillingResponse);
rpc CreateCheckoutSession(CreateCheckoutSessionRequest) returns (CreateCheckoutSessionResponse);
rpc CreateBillingPortalSession(CreateBillingPortalSessionRequest) returns (CreateBillingPortalSessionResponse);
rpc ChangeShape(ChangeShapeRequest) returns (ChangeShapeResponse);
}
// VM configuration — all fields use 0/empty as "use default"
message VmConfig {
uint32 vcpu = 1; // 0 = user quota or 2
uint64 memory_bytes = 2; // 0 = user quota or 8 GiB
uint64 disk_bytes = 3; // 0 = 100 GiB
SrfConfig srf = 4;
NetworkConfig network = 5;
repeated VolumeMount volumes = 6;
}
message SrfConfig {
optional uint32 auto_suspend_timeout_secs = 1; // unset = server default (30s for new, inherit for fork)
uint32 auto_destroy_timeout_secs = 2; // 0 = no auto-destroy
}
message NetworkConfig {
bool ssh = 1; // default: true
repeated ProxyEntry proxies = 2;
}
message ProxyEntry {
string name = 1;
uint32 port = 2; // 0 = auto-detect
}
message VolumeMount {
string disk_id = 1;
string mount_path = 2;
bool read_only = 3;
}
// VM management
message CreateVmRequest {
string name = 1;
string image_ref = 2;
reserved 3; // network_id removed — auto-created per user
reserved 4; // disk_bytes removed — fixed at 100 GB
repeated EnvVar env = 5;
repeated string cmd = 6;
string restart_policy = 7;
VmConfig config = 8; // omit for defaults (all zeros)
}
message EnvVar {
string key = 1;
string value = 2;
}
message CreateVmResponse {
string vm_id = 1;
string name = 2;
string public_ip = 3;
string url = 4;
string image = 5;
string status = 6;
uint64 boot_time_ms = 7;
}
message DestroyVmRequest { string vm_id = 1; }
message DestroyVmResponse {}
message StartVmRequest { string vm_id = 1; }
message StartVmResponse {}
message StopVmRequest { string vm_id = 1; }
message StopVmResponse {}
message RebootVmRequest { string vm_id = 1; }
message RebootVmResponse {}
message SuspendVmRequest { string vm_id = 1; }
message SuspendVmResponse {
uint64 suspend_us = 1;
}
message ResumeVmRequest { string vm_id = 1; }
message ResumeVmResponse {
uint64 resume_us = 1;
}
message GetVmRequest { string vm_id = 1; }
message GetVmResponse {
string vm_id = 1;
string name = 2;
string image_ref = 3;
string public_ip = 4;
string status = 5;
string restart_policy = 6;
uint64 disk_bytes = 7;
uint32 auto_suspend_timeout_secs = 8; // 0 = disabled/unset; always concrete on the wire
uint32 ssh_port = 9; // per-VM SSH port on the proxy IP (0 = not yet allocated)
}
message ListVmsRequest {}
message ListVmsResponse {
repeated GetVmResponse vms = 1;
}
// Raw TCP/UDP port forwards. Allocates a public port (40000–60000) on the VM's
// proxy, DNAT-forwarded to vm_port inside the VM. Up to 3 forwards per VM.
message ExposePortRequest { string vm_id = 1; uint32 vm_port = 2; string protocol = 3; } // protocol: "tcp" | "udp" | "both"
// dns is `<vm>.<zone>` and resolves to the VM's proxy IP — connect on public_port.
message ExposePortResponse {
string vm_name = 1;
string dns = 2;
uint32 public_port = 3;
uint32 vm_port = 4;
string protocol = 5;
}
// Remove the forward for (vm_id, vm_port), freeing its public port. Errors if
// nothing is exposed on that port. The response echoes what was removed.
message UnexposePortRequest { string vm_id = 1; uint32 vm_port = 2; }
message UnexposePortResponse { ExposePortResponse forward = 1; }
message ListExposedPortsRequest {}
message ListExposedPortsResponse { repeated ExposePortResponse forwards = 1; }
// Logs and exec
message StreamLogsRequest {
string vm_id = 1;
bool follow = 2;
}
message LogChunk {
bytes data = 1;
}
message ExecChunk {
bytes data = 1;
bool stdin = 2;
bool tty = 3;
string command = 4;
string vm_id = 5;
int32 exit_code = 6;
// PTY size. On the first chunk (with `tty = true`) these set the initial
// PTY geometry. On a subsequent chunk with `window_change = true` they
// signal a terminal resize. Zero falls back to 80x24.
uint32 cols = 7;
uint32 rows = 8;
bool window_change = 9;
// Server→client: the bytes in `data` came from the subprocess's stderr.
// Defaults to false (stdout). Old servers never set this — old clients
// ignore it. PTY-mode execs merge stderr into stdout at the kernel
// level (slave_fd), so this field is only meaningful for non-PTY execs.
bool is_stderr = 10;
// Client-utilities device id of the connecting machine; the proxy injects it
// into the VM session env as BOXD_DEVICE_ID so in-VM tools reach this device.
string device_id = 11;
}
// Networks
message CreateNetworkRequest {
string name = 1;
}
message CreateNetworkResponse {
string network_id = 1;
}
message ListNetworksRequest {}
message ListNetworksResponse {
repeated NetworkInfo networks = 1;
}
message NetworkInfo {
string network_id = 1;
string subnet = 2;
string status = 3;
}
// Domains
message BindDomainRequest {
string domain = 1;
string vm_id = 2;
}
message BindDomainResponse {}
message UnbindDomainRequest {
string domain = 1;
}
message UnbindDomainResponse {}
message ListDomainsRequest {}
message ListDomainsResponse {
repeated DomainInfo domains = 1;
}
message DomainInfo {
string domain = 1;
string vm_id = 2;
}
// Config
message GetConfigRequest {}
message GetConfigResponse {
string default_image = 1;
string zone = 2;
}
// Identity
message WhoamiRequest {}
message WhoamiResponse {
string user_id = 1;
repeated string pubkey_fingerprints = 2;
string default_network_id = 3;
BillingInfo billing = 4;
}
// Current billing state for the caller, mirrored from users.{tier, shape,
// stripe_*, subscription_status, past_due_since}. Empty stripe_*/past_due_since
// when the user has never upgraded; trial_end_unix is set during the 14-day
// Stripe-native trial.
message BillingInfo {
string tier = 1; // 'free' | 'individual'
string shape = 2; // '2x8' | '4x16' | '8x32' | '16x64'
string subscription_status = 3; // 'active' | 'trialing' | 'past_due' | 'canceled'
string stripe_customer_id = 4; // empty when never upgraded
string stripe_subscription_id = 5; // empty when on free tier
int64 past_due_since = 6; // 0 = not past due
uint32 max_vms = 7; // effective quota
}
// Static catalog of plans the website + console render. Mirrors the
// Stripe lookup_key set (individual-2x8 … individual-16x64). The server
// is the authority on the catalog so the public site and the API never
// drift on prices.
message ListPlansRequest {}
message ListPlansResponse {
repeated PlanInfo plans = 1;
}
message PlanInfo {
string tier = 1; // 'free' | 'individual'
string shape = 2; // '2x8' | '4x16' | '8x32' | '16x64'
uint32 monthly_eur = 3; // price in EUR cents, e.g. 4000 for €40
string lookup_key = 4; // 'individual-2x8' …
string description = 5; // human-readable
uint32 vcpu = 6;
uint64 memory_bytes = 7;
}
// Detailed billing view (same as Whoami.billing today; kept as its own
// RPC because Phase 3 will extend it with invoice history / next-renewal).
message GetBillingRequest {}
message GetBillingResponse {
BillingInfo billing = 1;
}
// Free → paid upgrade. Server: creates Stripe Customer if missing, creates
// Checkout Session for the requested shape with 14-day trial + automatic
// VAT, returns the URL. Client redirects browser to the URL. Webhook
// activates the subscription on completion (Phase 4).
message CreateCheckoutSessionRequest {
string shape = 1; // '4x16' | '8x32' | '16x64' (2x8 is free, not chargeable)
string success_url = 2; // optional override; server has a default
string cancel_url = 3; // optional override
}
message CreateCheckoutSessionResponse {
string checkout_url = 1;
}
// Paying user → manage payment/cancel. Server creates a Stripe Customer
// Portal session bound to user.stripe_customer_id; client redirects.
message CreateBillingPortalSessionRequest {
string return_url = 1; // optional override
}
message CreateBillingPortalSessionResponse {
string portal_url = 1;
}
// In-place shape swap (e.g. 4x16 → 8x32). Server calls
// Subscriptions.update on Stripe with the new price + proration. Webhook
// commits the new shape to Raft (Phase 4).
message ChangeShapeRequest {
string shape = 1; // '2x8' | '4x16' | '8x32' | '16x64'
}
message ChangeShapeResponse {
string shape = 1; // echoed back from Stripe response
string status = 2; // 'trialing' | 'active' | ...
}
// Tokens
message CreateTokenRequest {
uint64 expires_in_secs = 1;
}
message CreateTokenResponse {
string token = 1;
int64 expires_at = 2;
}
message ListTokensRequest {}
message ListTokensResponse {
repeated TokenInfo tokens = 1;
}
message TokenInfo {
string jti = 1;
int64 created_at = 2;
int64 expires_at = 3;
}
message RevokeTokenRequest {
string jti = 1;
}
message RevokeTokenResponse {}
// Fork
message ForkVmRequest {
string source_vm_id = 1;
string name = 2;
VmConfig config = 3; // omit = inherit from source (all zeros)
}
message ForkVmResponse {
string vm_id = 1;
string name = 2;
string public_ip = 3;
string url = 4;
string image = 5;
string status = 6;
string forked_from = 7;
uint64 boot_time_ms = 8;
}
// Proxies
message ListProxiesRequest {
string vm_name = 1;
}
message ListProxiesResponse {
repeated ProxyInfo proxies = 1;
}
message ProxyInfo {
string name = 1;
string vm_name = 2;
string domain = 3;
uint32 port = 4;
bool is_default = 5;
string port_display = 6;
}
message CreateProxyRequest {
string name = 1;
string vm_name = 2;
uint32 port = 3;
}
message CreateProxyResponse {
string name = 1;
string vm_name = 2;
string domain = 3;
uint32 port = 4;
}
message DeleteProxyRequest {
string name = 1;
string vm_name = 2;
}
message DeleteProxyResponse {}
message SetProxyPortRequest {
string name = 1;
string vm_name = 2;
string port = 3;
}
message SetProxyPortResponse {}
// File transfer
message UploadFileRequest {
string vm_id = 1;
string path = 2;
bytes data = 3;
}
message UploadFileResponse {
// Bytes the server confirmed it wrote (verified via stat after upload).
// Older servers that don't perform verification leave this as 0; CLIs
// should fall back to the local file size in that case.
uint64 bytes_written = 1;
}
// Streaming chunk for `UploadFileStream`. The first chunk on the stream
// carries `vm_id`, `path`, and `total_size` (set once); `data` may be empty
// or carry the first slice of file bytes. Subsequent chunks set `data` only
// — vm_id/path/total_size on later chunks are ignored. The stream end
// signals completion; the server has already exited its writer once
// `total_size` bytes have arrived.
message UploadFileChunk {
string vm_id = 1;
string path = 2;
uint64 total_size = 3;
bytes data = 4;
}
message DownloadFileRequest {
string vm_id = 1;
string path = 2;
}
message DownloadFileResponse {
bytes data = 1;
}
// --- Disks ---
message CreateDiskRequest {
string name = 1;
uint64 size_bytes = 2; // disk size in bytes
}
message CreateDiskResponse {
string disk_id = 1;
string name = 2;
uint64 size_bytes = 3;
string status = 4;
}
message ListDisksRequest {}
message ListDisksResponse {
repeated DiskInfo disks = 1;
}
message DiskInfo {
string disk_id = 1;
string name = 2;
uint64 size_bytes = 3;
string status = 4;
string worker_id = 5;
repeated DiskAttachment attachments = 6;
}
message DiskAttachment {
string vm_id = 1;
string vm_name = 2;
string mount_path = 3;
string mount_mode = 4; // "ro" or "rw"
}
message AttachDiskRequest {
string disk_id = 1;
string vm_id = 2;
string mount_path = 3;
bool read_only = 4;
}
message AttachDiskResponse {}
message DetachDiskRequest {
string disk_id = 1;
string vm_id = 2;
}
message DetachDiskResponse {}
message DestroyDiskRequest {
string disk_id = 1;
}
message DestroyDiskResponse {}
message SetAutoSuspendTimeoutRequest {
string vm_id = 1; // accepts VM name or id; resolved server-side
uint32 timeout_secs = 2; // 0 = disable
}
message SetAutoSuspendTimeoutResponse {}
message SetAutoHibernateTimeoutRequest {
string vm_id = 1; // accepts VM name or id; resolved server-side
uint32 timeout_secs = 2; // 0 = disable
}
message SetAutoHibernateTimeoutResponse {}
// --- API Keys ---
message CreateApiKeyRequest {
string name = 1;
uint64 expires_in_secs = 2; // 0 = no expiry
}
message CreateApiKeyResponse {
string id = 1;
string api_key = 2; // raw key, shown once — never stored
int64 expires_at = 3; // 0 = no expiry
}
message ListApiKeysRequest {}
message ListApiKeysResponse {
repeated ApiKeyInfo keys = 1;
}
message ApiKeyInfo {
string id = 1;
string name = 2;
string key_prefix = 3;
int64 created_at = 4;
int64 last_used_at = 5; // 0 = never
int64 expires_at = 6; // 0 = no expiry
}
message DeleteApiKeyRequest {
string id = 1;
}
message DeleteApiKeyResponse {}
// NOTE: the client-utilities DeviceBridge service lives in `device.proto`
// (same package) — deliberately out of this file so the SDK regen scripts,
// which compile only api.proto, never ship it in the TS/Python stubs.
Authentication
Two-step: long-lived API key → short-lived JWT → bearer token on every gRPC call.1. Create an API key
API keys are issued from the boxd console. Sign in at boxd.sh, open the API keys page, and create one. The raw key is shown once — copy it immediately. Format:bxd_ followed by ~40 base62 characters.
You can also create them via the CLI once you’re logged in:
boxd auth login # GitHub OAuth
boxd auth keys create my-app
2. Exchange for a JWT
Send the API key to the exchange endpoint over HTTPS:curl -X POST https://boxd.sh/api/v1/auth/token \
-H "Content-Type: application/json" \
-d '{"api_key":"bxd_..."}'
{
"token": "eyJhbGciOi...",
"expires_at": 1735689600,
"user_id": "gh-username"
}
3. Send it on every gRPC call
authorization: Bearer eyJhbGciOi...
Hello world
- grpcurl
- Go
- Node / TypeScript
- Python
Easiest way to verify auth and reachability — no proto file needed (server reflection is on).Install:
# Set your JWT
export BOXD_JWT="$(curl -s -X POST https://boxd.sh/api/v1/auth/token \
-H 'Content-Type: application/json' \
-d '{"api_key":"bxd_..."}' | jq -r .token)"
# Whoami
grpcurl -plaintext \
-H "authorization: Bearer $BOXD_JWT" \
boxd.sh:9443 boxd.api.v1.BoxdApi/Whoami
# Create a VM
grpcurl -plaintext \
-H "authorization: Bearer $BOXD_JWT" \
-d '{"name": "hello-grpc"}' \
boxd.sh:9443 boxd.api.v1.BoxdApi/CreateVm
# List VMs
grpcurl -plaintext \
-H "authorization: Bearer $BOXD_JWT" \
boxd.sh:9443 boxd.api.v1.BoxdApi/ListVms
brew install grpcurl or github.com/fullstorydev/grpcurl.Save the proto from above as Hello world (
gen/api.proto, then generate stubs:cd gen
protoc --go_out=. --go_opt=paths=source_relative \
--go-grpc_out=. --go-grpc_opt=paths=source_relative \
api.proto
main.go):package main
import (
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"strings"
boxdv1 "yourmodule/gen"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/metadata"
)
func exchange(apiKey string) string {
body := strings.NewReader(`{"api_key":"` + apiKey + `"}`)
resp, err := http.Post("https://boxd.sh/api/v1/auth/token",
"application/json", body)
if err != nil { log.Fatal(err) }
defer resp.Body.Close()
var out struct{ Token string }
json.NewDecoder(resp.Body).Decode(&out)
return out.Token
}
func main() {
jwt := exchange("bxd_...")
conn, err := grpc.NewClient("boxd.sh:9443",
grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil { log.Fatal(err) }
defer conn.Close()
client := boxdv1.NewBoxdApiClient(conn)
ctx := metadata.AppendToOutgoingContext(context.Background(),
"authorization", "Bearer "+jwt)
me, err := client.Whoami(ctx, &boxdv1.WhoamiRequest{})
if err != nil { log.Fatal(err) }
fmt.Println("user:", me.UserId)
vm, err := client.CreateVm(ctx, &boxdv1.CreateVmRequest{
Name: "hello-grpc",
})
if err != nil { log.Fatal(err) }
fmt.Printf("created %s at %s (%dms)\n",
vm.Name, vm.Url, vm.BootTimeMs)
}
Save the proto from above as Hello world (
./api.proto, then install runtime deps:npm install @grpc/grpc-js @grpc/proto-loader
hello.ts) — uses dynamic loading, no codegen step:import * as grpc from '@grpc/grpc-js';
import * as protoLoader from '@grpc/proto-loader';
async function exchange(apiKey: string): Promise<string> {
const r = await fetch('https://boxd.sh/api/v1/auth/token', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ api_key: apiKey }),
});
const { token } = await r.json();
return token;
}
async function main() {
const jwt = await exchange('bxd_...');
const def = protoLoader.loadSync('./api.proto', {
keepCase: true, longs: String, enums: String, defaults: true,
});
const proto = grpc.loadPackageDefinition(def) as any;
const meta = new grpc.Metadata();
meta.set('authorization', `Bearer ${jwt}`);
const client = new proto.boxd.api.v1.BoxdApi(
'boxd.sh:9443',
grpc.credentials.createInsecure(),
);
client.Whoami({}, meta, (err: any, me: any) => {
if (err) throw err;
console.log('user:', me.user_id);
client.CreateVm({ name: 'hello-grpc' }, meta, (err: any, vm: any) => {
if (err) throw err;
console.log(`created ${vm.name} at ${vm.url} (${vm.boot_time_ms}ms)`);
});
});
}
main();
Save the proto from above as Hello world (
./api.proto, then:pip install grpcio grpcio-tools requests
python -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. api.proto
hello.py):import grpc
import requests
import api_pb2 as pb
import api_pb2_grpc as rpc
def exchange(api_key: str) -> str:
r = requests.post(
"https://boxd.sh/api/v1/auth/token",
json={"api_key": api_key},
)
r.raise_for_status()
return r.json()["token"]
def main():
jwt = exchange("bxd_...")
meta = (("authorization", f"Bearer {jwt}"),)
with grpc.insecure_channel("boxd.sh:9443") as ch:
client = rpc.BoxdApiStub(ch)
me = client.Whoami(pb.WhoamiRequest(), metadata=meta)
print("user:", me.user_id)
vm = client.CreateVm(pb.CreateVmRequest(name="hello-grpc"), metadata=meta)
print(f"created {vm.name} at {vm.url} ({vm.boot_time_ms}ms)")
if __name__ == "__main__":
main()
Errors
Standard gRPC status codes. The most common ones you’ll hit:| Code | When |
|---|---|
UNAUTHENTICATED | Missing/malformed/expired JWT, or authorization metadata not set |
NOT_FOUND | VM, disk, template, or domain doesn’t exist |
RESOURCE_EXHAUSTED | Per-user VM quota reached |
INVALID_ARGUMENT | Bad request shape (e.g. invalid VM name, conflicting fields) |
INTERNAL | Server-side error |
Status.message() is human-readable and safe to surface to users.
Streaming RPCs
Three RPCs use streams:StreamLogs— server-streaming, emitsLogChunkmessages as the VM produces output. Setfollow=trueto keep the stream open after current logs flush.UploadFileStream— client-streaming, lifts the 4 MiB single-message gRPC cap ofUploadFile. The firstUploadFileChunkcarriesvm_id,path, andtotal_size; subsequent chunks carry bytes only. The server reads exactlytotal_sizebytes and returns the confirmed byte count.Exec— bidirectional. First message must containvm_idandcommand; subsequent client messages withstdin=truepipe stdin, orwindow_change=truewithcols/rowsresize a PTY. Server messages carrydatawithis_stderrset totruefor stderr chunks andfalse(default) for stdout. PTY-mode execs merge stderr into stdout at the kernel, sois_stderris only set for non-PTY execs. Final server message hasexit_codeset. Clients close their send half of the bidi stream to signal stdin EOF to the subprocess (the proxy translates this toCHANNEL_EOFon the underlying SSH channel).
What’s next
CLI
Same API, no codegen — useful for shell scripting and one-offs.
Primitives: Machines
Concepts behind
CreateVm / ForkVm / SuspendVm.