Documentation Index Fetch the complete documentation index at: https://docs.boxd.sh/llms.txt
Use this file to discover all available pages before exploring further.
The boxd CLI, SSH server, and SDKs are all clients of one thing: the public gRPC API at 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
Transport: HTTP/2 cleartext (h2c). No TLS on this port โ auth is short-lived JWTs.
Reflection: gRPC server reflection is enabled, so tools like grpcurl work without the proto file.
Service: boxd.api.v1.BoxdApi
The proto
Copy the full service definition into a local api.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
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 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 );
rpc DownloadFile ( DownloadFileRequest ) returns ( DownloadFileResponse );
rpc SuspendVm ( SuspendVmRequest ) returns ( SuspendVmResponse );
rpc ResumeVm ( ResumeVmRequest ) returns ( ResumeVmResponse );
rpc CreateTemplate ( CreateTemplateRequest ) returns ( CreateTemplateResponse );
rpc ListTemplates ( ListTemplatesRequest ) returns ( ListTemplatesResponse );
rpc DeleteTemplate ( DeleteTemplateRequest ) returns ( DeleteTemplateResponse );
rpc CreateVmFromTemplate ( CreateVmFromTemplateRequest ) returns ( CreateVmFromTemplateResponse );
rpc SetAutoSuspendTimeout ( SetAutoSuspendTimeoutRequest ) returns ( SetAutoSuspendTimeoutResponse );
// 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 );
}
// 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
}
message ListVmsRequest {}
message ListVmsResponse {
repeated GetVmResponse vms = 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 ;
}
// 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 ;
}
// 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 {}
message DownloadFileRequest {
string vm_id = 1 ;
string path = 2 ;
}
message DownloadFileResponse {
bytes data = 1 ;
}
// Templates
message CreateTemplateRequest {
string name = 1 ;
string image_ref = 2 ;
VmConfig config = 3 ;
}
message CreateTemplateResponse {
string template_id = 1 ;
string name = 2 ;
string status = 3 ;
}
message ListTemplatesRequest {}
message ListTemplatesResponse {
repeated TemplateInfo templates = 1 ;
}
message TemplateInfo {
string template_id = 1 ;
string name = 2 ;
string image_ref = 3 ;
string status = 4 ;
uint32 vcpu = 5 ;
uint64 memory_bytes = 6 ;
}
message DeleteTemplateRequest {
string template_id = 1 ;
}
message DeleteTemplateResponse {}
message CreateVmFromTemplateRequest {
string template_id = 1 ;
string name = 2 ;
VmConfig config = 3 ;
}
message CreateVmFromTemplateResponse {
string vm_id = 1 ;
string name = 2 ;
string public_ip = 3 ;
string url = 4 ;
string status = 5 ;
uint64 boot_time_ms = 6 ;
}
// --- 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 {}
// --- 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 {}
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 login # GitHub OAuth
boxd api-key create --name=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"
}
The JWT is short-lived (default ~1 hour). Re-exchange before expiry. Rate-limited per source IP.
3. Send it on every gRPC call
authorization: Bearer eyJhbGciOi...
Standard gRPC metadata. Most generated clients expose this as a per-call interceptor or call option.
Hello world
grpcurl
Go
Node / TypeScript
Python
Easiest way to verify auth and reachability โ no proto file needed (server reflection is on). # 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
Install: brew install grpcurl or github.com/fullstorydev/grpcurl . Save the proto from above as 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
Hello world (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 ( %d ms) \n " ,
vm.Name, vm.Url, vm.BootTimeMs)
}
Save the proto from above as ./api.proto, then install runtime deps: npm install @grpc/grpc-js @grpc/proto-loader
Hello world (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 ./api.proto, then: pip install grpcio grpcio-tools requests
python -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. api.proto
Hello world (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()
Standard gRPC status codes. The most common ones youโll hit:
Code When UNAUTHENTICATEDMissing/malformed/expired JWT, or authorization metadata not set NOT_FOUNDVM, disk, template, or domain doesnโt exist RESOURCE_EXHAUSTEDPer-user VM quota reached INVALID_ARGUMENTBad request shape (e.g. invalid VM name, conflicting fields) INTERNALServer-side error
The error message in Status.message() is human-readable and safe to surface to users.
Streaming RPCs
Two RPCs use streams:
StreamLogs โ server-streaming, emits LogChunk messages as the VM produces output. Set follow=true to keep the stream open after current logs flush.
Exec โ bidirectional. First message must contain vm_id and command; subsequent client messages with stdin=true pipe stdin; server messages return stdout/stderr data. Final server message has exit_code set.
Whatโs next
External CLI Same API, no codegen โ useful for shell scripting and one-offs.
Primitives: Machines Concepts behind CreateVm / ForkVm / SuspendVm.