Menu

Go SDK

The metalhost-sdk module is the public API contract: protobuf types, Connect-RPC clients for every customer-facing service, and a thin metalhost.Config auth helper. The CLI is built on this module. Read Concepts first if you're new to the platform model.

Install

go get github.com/AES-Services/metalhost-sdk@latest

Module: github.com/AES-Services/metalhost-sdk · Repo: github.com/AES-Services/metalhost-sdk

Dependencies you'll also need in your go.mod:

go get connectrpc.com/connect@latest

Environment variables

VariableRequiredPurpose
METALHOST_API_KEYYesBearer token for every RPC
METALHOST_ENDPOINTNoAPI base URL (default https://api.metalhost.net)
METALHOST_PROJECTFor VM opsDefault project, e.g. projects/my-app
METALHOST_REGIONFor VM opsDefault datacenter, e.g. datacenters/us-dal-1

Mint an API key in the dashboard: Developers → API keys → Create. Fund the wallet before creating VMs.

Complete program

This program creates a VM, waits for provisioning, and prints an SSH command. It is the SDK equivalent of the CLI quickstart.

// cmd/metalhost-demo/main.go — end-to-end: create VM, wait, print SSH target.
package main

import (
    "context"
    "fmt"
    "log"
    "net/http"
    "os"
    "time"

    "connectrpc.com/connect"
    "github.com/AES-Services/metalhost-sdk/metalhost"
    computev1 "github.com/AES-Services/metalhost-sdk/gen/go/aes/compute/v1"
    computev1connect "github.com/AES-Services/metalhost-sdk/gen/go/aes/compute/v1/computev1connect"
    opsv1 "github.com/AES-Services/metalhost-sdk/gen/go/aes/ops/v1"
    opsv1connect "github.com/AES-Services/metalhost-sdk/gen/go/aes/ops/v1/opsv1connect"
)

func main() {
    cfg := metalhost.Config{
        Endpoint: env("METALHOST_ENDPOINT", "https://api.metalhost.net"),
        APIKey:   os.Getenv("METALHOST_API_KEY"),
    }
    if cfg.APIKey == "" {
        log.Fatal("METALHOST_API_KEY is required")
    }
    httpClient := &http.Client{
        Transport: cfg.RoundTripper(http.DefaultTransport),
        Timeout:   60 * time.Second,
    }
    base := cfg.BaseURL()
    compute := computev1connect.NewComputeServiceClient(httpClient, base)
    ops := opsv1connect.NewOperationsServiceClient(httpClient, base)
    ctx := context.Background()

    project := env("METALHOST_PROJECT", "")
    region := env("METALHOST_REGION", "datacenters/us-dal-1")
    sshKey := env("METALHOST_SSH_KEY", project+"/ssh-keys/laptop")
    hostname := env("METALHOST_VM_NAME", "web-1")

    createResp, err := compute.CreateVirtualMachine(ctx, connect.NewRequest(
        &computev1.CreateVirtualMachineRequest{
            Manifest: &computev1.VirtualMachineManifest{
                ApiVersion: "compute.metalhost.io/v1",
                Kind:       "VirtualMachine",
                Metadata: &computev1.VirtualMachineMetadata{
                    Name: hostname, Project: project,
                },
                Spec: &computev1.VirtualMachineSpec{
                    Region: region,
                    Compute: &computev1.VMComputeSpec{
                        CpuClass: "cascadelake", Vcpus: 2, RamGib: 8,
                    },
                    Boot:    &computev1.VMBootSpec{Image: "ubuntu-24-04", DiskGib: 80},
                    Network: &computev1.VMNetworkSpec{PublicIpv4: true},
                    Users: []*computev1.UserSpec{{
                        Name: "ubuntu", SshKeys: []string{sshKey},
                    }},
                },
            },
        },
    ))
    if err != nil {
        log.Fatal(err)
    }
    op := createResp.Msg.GetOperation()
    if op == nil {
        log.Fatal("CreateVirtualMachine returned no operation")
    }
    final, err := waitOperation(ctx, ops, op.GetName(), 10*time.Minute)
    if err != nil {
        log.Fatal(err)
    }
    if final.GetState() != opsv1.State_STATE_SUCCEEDED {
        log.Fatalf("provision failed: %s", final.GetErrorMessage())
    }

    vmName := final.GetMetadata()["virtual_machine_name"]
    if vmName == "" {
        vmName = project + "/virtual-machines/" + hostname
    }
    vmResp, err := compute.GetVirtualMachine(ctx, connect.NewRequest(
        &computev1.GetVirtualMachineRequest{Name: vmName},
    ))
    if err != nil {
        log.Fatal(err)
    }
    vm := vmResp.Msg.GetVirtualMachine()
    fmt.Printf("ssh %s@%s\n", vm.GetLinuxUsername(), vm.GetPublicIpv4())
}

func waitOperation(ctx context.Context, ops opsv1connect.OperationsServiceClient, name string, timeout time.Duration) (*opsv1.Operation, error) {
    deadline := time.Now().Add(timeout)
    for {
        resp, err := ops.GetOperation(ctx, connect.NewRequest(&opsv1.GetOperationRequest{Name: name}))
        if err != nil {
            return nil, err
        }
        op := resp.Msg.GetOperation()
        switch op.GetState() {
        case opsv1.State_STATE_SUCCEEDED, opsv1.State_STATE_FAILED, opsv1.State_STATE_CANCELLED:
            return op, nil
        }
        if time.Now().After(deadline) {
            return nil, fmt.Errorf("timeout waiting for %s", name)
        }
        time.Sleep(2 * time.Second)
    }
}

func env(key, fallback string) string {
    if v := os.Getenv(key); v != "" {
        return v
    }
    return fallback
}
export METALHOST_API_KEY=mh_live_...
export METALHOST_PROJECT=projects/my-app
export METALHOST_SSH_KEY=projects/my-app/ssh-keys/laptop
go run ./cmd/metalhost-demo

Configure clients

Every Connect service client takes an *http.Client and base URL. Wrap the transport with cfg.RoundTripper so the API key is sent on every request:

cfg := metalhost.Config{
    Endpoint: os.Getenv("METALHOST_ENDPOINT"), // https://api.metalhost.net
    APIKey:   os.Getenv("METALHOST_API_KEY"),
}
httpClient := &http.Client{
    Transport: cfg.RoundTripper(http.DefaultTransport),
}
compute := computev1connect.NewComputeServiceClient(httpClient, cfg.BaseURL())

Create one *http.Client and reuse it across all service clients (compute, ops, storage, network, etc.).

HTTP protocol

Every RPC is:

  • Method: POST
  • Path: /aes.<domain>.v1.<Service>/<Method>
  • Header: Authorization: Bearer <API_KEY>
  • Header: Content-Type: application/json
  • Body: proto-JSON request message (camelCase fields)

Wrap requests with connect.NewRequest and a pointer to the request struct. Read responses from resp.Msg. Full schema for every method is in the OpenAPI viewer.

Resource names

Pass fully-qualified names or bare slugs (CLI expands slugs; in SDK you should use full names). See Concepts → Resource names.

Topic guides

The getting-started sections below cover install, auth, protocol, and errors. Per-resource guides have field tables, constraints, and full code samples:

Long-running operations

VM create, delete, resize, clone, and reimage return an operation field. Poll OperationsService/GetOperation until state is SUCCEEDED, FAILED, or CANCELLED.

Important metadata keys on success:

  • virtual_machine_name — full VM resource name after create/clone

The complete program above includes a waitOperation helper. The SDK does not ship a built-in wait function — copy that loop or use the CLI's ops wait for debugging.

Pagination

List RPCs accept pageSize and pageToken. Pass the nextPageToken from the prior response to fetch the next page. The CLI's --all flag loops this for you.

Error handling

Failed RPCs return a connect.Error with a gRPC-style code (NotFound, InvalidArgument, FailedPrecondition, PermissionDenied, etc.):

resp, err := compute.GetVirtualMachine(ctx, req)
if err != nil {
    if connectErr := new(connect.Error); errors.As(err, &connectErr) {
        // connectErr.Code() is connect.Code (NotFound, InvalidArgument, etc.)
        // connectErr.Message() is the server error string
    }
    return err
}

Common causes:

  • FailedPrecondition — wallet balance too low for create or prepaid term
  • InvalidArgument — manifest validation failed (missing cpuClass, bad ratio, etc.)
  • NotFound — resource name doesn't exist or wrong project scope
  • PermissionDenied — API key lacks access to the project

Services and key RPCs

Import clients from gen/go/aes/<domain>/v1/*v1connect. Every RPC path is listed in the OpenAPI spec.

ClientKey RPCs
ComputeServiceClientCreate/Get/List/Delete/Start/Stop/Restart/Resize/Reimage/Clone VirtualMachine; Snapshot; GetVMMetrics; OpenConsole
SSHKeysServiceClientCreate/List/Delete SSHKey
OperationsServiceClientGet/List Operation
StorageServiceClientCreate/Get/List/Delete Disk; File shares
NetworkServiceClientCreate/Get/List Network; Firewall rules
CatalogServiceClientListDatacenters; QuoteVirtualMachine; GetVMCapacity
ProjectServiceClientCreate/Get/List Project; ListOrganizations
WalletServiceClientGetBalance; CreateTopUp; ListUsage; Invoices
IamServiceClientGetCallerIdentity; Create/Revoke ApiKey; Login; Org members
BareMetalServiceClientListAvailable; Create/Get/Release instance; Power; ISO
WebhooksServiceClientCreate/List/Delete subscriptions; List deliveries
QuotaServiceClientGet org/project quotas
AuditServiceClientSearchEvents
SupportServiceClientCreate/List tickets
HealthServiceClientCheck

HTTP/JSON without Go

Same request shape as the SDK — useful for quick tests and non-Go integrations:

curl -sS -X POST "$METALHOST_ENDPOINT/aes.compute.v1.ComputeService/ListVirtualMachines" \
  -H "Authorization: Bearer $METALHOST_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"projectName":"projects/my-app"}'

What's next