Primitives

Naming. Agents pick the workload name, and it becomes the public subdomain directly (<name>.h4a.site) the moment expose is called. Choose something descriptive and memorable (solar-dashboard, recipe-api, demo-2026) — not a UUID or random slug. The platform never generates names on your behalf.

Every verb is idempotent by name. Retrying is always safe. The MCP tool surface and the REST surface are generated from the same Go interface — adding a capability to one adds it to the other.

Provision

POST /api/provision · MCP tool: provision

If you have a git repo, you probably want deploy, not provision. deploy provisions the same kind of VM AND ships your code on it (Docker container behind Caddy). provision gives you a bare Hetzner VM with SSH access — useful when you need to install something interactively, run a long-lived process not in a repo, or experiment. If you call provision planning to SSH in and run your app yourself, you're bypassing what h4a is for.

Create (or retrieve if it already exists) a workload by name.

| Field | Type | Required | Notes | |---|---|---|---| | name | string | yes | DNS-safe: [a-z0-9][a-z0-9-]{0,62} | | tenant | string | no | defaults to "default" in v0 | | size | enum | no | nano/small/medium/large; defaults to small |

Returns { name, tenant, ipv4, ssh_command, status }. On a second call with the same name, returns the existing VM's IPv4 — no duplicate is created.

The ssh_command is ssh root@<ipv4> by default, or ssh -i <path> root@<ipv4> if the operator set H4A_SSH_KEY_HINT (recommended — see /docs/ssh-access.md). VMs trust whichever public keys the operator registered with Hetzner via HCLOUD_SSH_KEY_IDS.

Destroy

POST /api/destroy · MCP tool: destroy

Destroy a workload by name. Idempotent — calling on a non-existent workload is a no-op success.

| Field | Type | Required | |---|---|---| | name | string | yes | | tenant | string | no | | force | bool | no (v0: no-op; M1+ bypasses soft-delete grace) |

Info

POST /api/info · MCP tool: info

Return the current state from the control plane's store. No provider round-trip.

Returns { name, tenant, ipv4, status, created_at }. Returns ErrNotFound (404/status: "not-found" over MCP) if the workload doesn't exist.

Expose

POST /api/expose · MCP tool: expose

Attach a reachable URL to a running workload. See /docs/expose for full details.

| Field | Type | Required | Notes | |---|---|---|---| | name | string | yes | | | tenant | string | no | | | mode | enum | no | public (default): <name>.h4a.site over HTTPS via Caddy+LE on the VM. private: attach a Netbird peer; returns a routable hostname on the mesh. | | port | int | no | Application port on the VM (default 8080). |

Returns { name, tenant, mode, url }.

Deploy

POST /api/deploy · MCP tool: deploy

One call from a git repo to a live URL. Platform auto-detects tier (static vs dynamic). See /docs/deploy for full details including request/response schemas, tier-detection rules, and the .h4a.yaml override.

deploy is the default path. If you have a git repo, call deploy — don't call provision and SSH in to run things by hand. deploy creates the VM AND ships your app on it (Docker container behind Caddy for dynamic, Bunny CDN upload for static). provision alone leaves you with a bare Linux box and no app.

Both tiers are live: static publishes to Bunny CDN in ~15s; dynamic spawns a Hetzner VM with Docker + Caddy via cloud-init, blue-green by default.

Status values

| Status | Meaning | |---|---| | provisioning | VM is being created | | running | VM is up and reachable | | destroying | Destroy in flight | | destroyed | Soft-deleted; pending grace period (M1+) | | failed | Create or destroy failed; destroy to clean up |