Skip to content
Pre-release. HostCraft is in testing and not yet for sale. These docs describe the current build and may change. Join the waitlist to hear when it launches.

Developer reference

HostCraft exposes one API over two transports, plus outbound automation:

  • REST under /api/v1/ (JSON over HTTPS through Caddy, port 2087), with server-sent event streams for workflow progress.
  • gRPC on the local Unix socket /run/hostcraft/hostcraftd.sock, for the server's own CLI tooling and scripts running on the box. The proto files in daemon/proto/hostcraft/v1/ are the contract; every unary RPC is served by the same handler as its REST route. See gRPC.
  • Webhooks and hook scripts for events the panel emits.

All of them share the same permission model.

Panel users authenticate via the login flow at https://<panel_hostname>/login. The server sets an hc_session cookie (name configurable via server.cookie_name). Cookie-based sessions work for browser clients and curl sessions where you maintain the cookie jar. Sessions are database-backed with instant revocation.

Terminal window
# Authenticate and save the session cookie
curl -fsSL -c cookies.txt -X POST \
https://panel.example.com/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"[email protected]","password":"<password>"}'
# Use the saved cookie
curl -fsSL -b cookies.txt \
https://panel.example.com/api/v1/sites

API tokens are the preferred authentication method for programmatic access. Create a token under Settings -> Developers -> API tokens, or with a signed-in session:

Terminal window
curl -fsSL -X POST \
-b cookies.txt \
-H "Content-Type: application/json" \
https://panel.example.com:2087/api/v1/auth/api-tokens \
-d '{
"name": "ci-deploy",
"scopes": ["WebSiteRead", "WebSiteCreate"],
"expires_at": "2027-01-01T00:00:00Z"
}'

Scopes are permission names from internal/auth/perms (GET /api/v1/auth/api-tokens/scopes lists the ones you may grant). A token holds the intersection of its scopes and its owner's role. Some permissions can never be put on a token: account deletion, admin user and credential management, update install and rollback, cluster topology changes and security policy writes need an interactive session. The response contains the plaintext secret (hcp_live_...) exactly once; store it securely.

Use the token in the Authorization header:

Terminal window
curl -fsSL \
-H "Authorization: Bearer hcp_live_..." \
-H "Accept: application/json" \
https://panel.example.com/api/v1/sites

The same token authenticates gRPC calls (authorization: Bearer hcp_live_... metadata). Tokens cannot call the /api/v1/auth/* credential and session endpoints, except GET /api/v1/auth/me.

Surface Address
REST https://<panel_hostname>:2087/api/v1/
Workflow progress (server-sent events) https://<panel_hostname>:2087/ws/workflows
Web terminal (WebSocket) wss://<panel_hostname>:2087/ws/terminal
gRPC unix:///run/hostcraft/hostcraftd.sock (local only; optional loopback TCP via server.grpc_addr)

All REST endpoints accept and return application/json. Fields use snake_case.

Success responses use the HTTP status codes as the primary signal:

Status Meaning
200 Read or update succeeded
201 Resource created
202 Accepted; long-running operation started (workflow-backed)
204 Deleted; no response body
400 Validation error; see error envelope
401 Not authenticated
403 Authenticated but not authorised for this action (or reauth_required)
404 Resource not found, or owned by another tenant
409 Conflict (already exists, in use)
423 Account locked while it moves between cluster nodes
429 Rate limit exceeded
503 The module behind the route is not enabled on this server
500 Internal server error (file a bug)

All error responses share one envelope:

{
"code": "username_taken",
"message": "Conflict",
"detail": "domains.account: username bob is taken"
}

code is a stable machine-readable string. message is the HTTP status text. detail is human-readable and may change between versions. Every response carries an X-Trace-Id header (send your own to correlate); include it in support tickets.

List endpoints return an object with one array (for example {"accounts": [...]}), scoped to what the caller may see. Endpoints that page take limit and offset query parameters. Many list endpoints filter by account_id.

Workflow-backed operations answer 202 Accepted with a workflow_id. Read the run with GET /api/v1/workflows/{id}, follow every run you may see on the server-sent event stream GET /ws/workflows, or use WorkflowService.StreamWorkflow over gRPC, which can follow one run until it finishes.

Terminal window
curl -fsSL -H "Authorization: Bearer hcp_live_..." \
https://panel.example.com:2087/api/v1/workflows/<workflow_id>

Destructive operations (deleting an account, domain, site, database or backup repository, admin user changes) need a session that re-entered its password in the last few minutes (POST /api/v1/auth/reauth) and otherwise answer 403 with code reauth_required. API tokens cannot satisfy this. Local root over the gRPC socket can.

API token requests are limited per token and per client address: a burst of 60, then 10 requests per second. Over the limit the API answers 429 Too Many Requests with Retry-After: 1 (gRPC: RESOURCE_EXHAUSTED). Login and other unauthenticated auth endpoints have their own stricter per-address limit.

The current version is v1 in the URL prefix. Breaking changes go to v2 with at least 12 months of overlap. Non-breaking additions (new fields, new endpoints) ship in v1 immediately.

hostcraftd serves every service in daemon/proto/hostcraft/v1/ over gRPC. The API reference lists each method with the REST route behind it and the permission it requires.

Setting Default Notes
server.grpc_socket /run/hostcraft/hostcraftd.sock Unix socket, mode 0660. Empty disables it.
server.grpc_socket_group hostcraft-admin Group that may open the socket. The installer creates it.
server.grpc_addr empty Optional TCP listener. Loopback addresses only; the daemon refuses to start it on any other address.

There is no TLS on either listener: the API never leaves the host. Use REST through Caddy for remote access.

  • Local root. A client running as uid 0 on the Unix socket is identified by the kernel (SO_PEERCRED) and acts as a super_admin named root. Root also satisfies the re-authentication step that destructive calls demand of browser sessions, so it can delete accounts and manage admin users. GetMe reports it as user root.
  • API token. Every other caller (a member of hostcraft-admin on the socket, or any caller on grpc_addr) must send authorization: Bearer hcp_live_... metadata. The token resolves through the same store, scopes, rate limits and disabled-user checks as REST.

Unauthenticated callers get UNAUTHENTICATED, except the standard grpc.health.v1.Health service. Server reflection is enabled for authenticated callers, so grpcurl works without local proto files.

An interceptor checks each method against its permission (the (hostcraft.v1.authz) option in the proto, compiled into a table; a method without a rule is refused). The handler behind the call then applies the same fine-grained checks as REST: tenant ownership (another tenant's resource is NOT_FOUND), plan limits, sub-user grants and licence gates. Every mutating call, and every refused call, is written to the audit log as action grpc.<Service>.<Method> with the caller, outcome and trace id.

REST errors map onto gRPC status codes, and the REST code is attached as a google.rpc.ErrorInfo detail (reason, domain hostcraft.io, metadata http_status). The status message is <code>: <detail>.

HTTP gRPC
400, 413, 422 INVALID_ARGUMENT
401 UNAUTHENTICATED
403 PERMISSION_DENIED
404 NOT_FOUND
409 ALREADY_EXISTS
412, 423 FAILED_PRECONDITION
429 RESOURCE_EXHAUSTED
501, 405 UNIMPLEMENTED
502, 503 UNAVAILABLE
504 DEADLINE_EXCEEDED
other 5xx INTERNAL

Request and response messages mirror the REST JSON exactly, including field names. A few nested messages (account limits, analytics rows) carry Go-style capitalised names because that is what the REST body contains. Timestamps are google.protobuf.Timestamp; an unset time is absent. optional fields are only sent when set, which is how partial updates (Update* methods) leave the other fields alone.

Method Kind Use
WorkflowService.StreamWorkflow server Follow one run (id) until it finishes, or every run you may see
ClusterService.StreamClusterEvents server Node joined, left and health events
RestoreService.UploadSource client Upload a cPanel cpmove archive in chunks (up to 8 GiB)
TerminalService.Attach bidirectional Interactive shell as an account's Unix user
FilesService.DownloadFile unary Whole file in one message, up to 256 MiB; raise the client's receive limit above 4 MiB
Terminal window
SOCK=/run/hostcraft/hostcraftd.sock
# As root on the server: list services, create and read an account
sudo grpcurl -plaintext -unix $SOCK list
sudo grpcurl -plaintext -unix $SOCK hostcraft.v1.AccountService/CreateAccount \
-d '{"username":"bob","email":"[email protected]","password":"S3cret-pass-2026"}'
sudo grpcurl -plaintext -unix $SOCK hostcraft.v1.AccountService/ListAccounts
sudo grpcurl -plaintext -unix $SOCK hostcraft.v1.WorkflowService/StreamWorkflow -d '{"id":"<workflow_id>"}'
# As a member of hostcraft-admin, with an API token
grpcurl -plaintext -unix $SOCK -H "authorization: Bearer $HOSTCRAFT_TOKEN" \
hostcraft.v1.DomainService/ListDomains -d '{"account_id":"<account_id>"}'

To generate a client, compile the protos with protoc (they depend only on the well-known types and hostcraft/v1/options.proto). scripts/gen-proto.sh regenerates the Go code, the server bindings and these reference pages after a proto change.

Webhooks deliver outbound notifications to an HTTP(S) endpoint you configure. Webhooks are server-wide configuration: only server admins (permissions WebhookCreate, WebhookRead, WebhookUpdate, WebhookDelete, WebhookTest) can manage them.

Event When it fires
account.created, account.suspended, account.deleted Account workflows succeed
site.created, site.deleted Website workflows succeed
site.ssl_issued A certificate is issued, renewed or installed
domain.created, domain.deleted Domain workflows succeed
dns.zone_created, dns.zone_deleted DNS zone workflows succeed
mail.domain_enabled, mail.mailbox_created, mail.mailbox_deleted, mail.dkim_rotated Mail workflows succeed
db.database_created, db.database_deleted Database workflows succeed
backup.run_completed, backup.run_failed, backup.restore_completed Backup runs and restores
migration.cpanel_completed, migration.cpanel_failed cPanel account migrations
wordpress.installed A WordPress install finishes
workflow.run_completed, workflow.run_failed Any workflow run finishes (failed means rolled back)
webhook.ping Only sent by the Test button or POST /api/v1/webhooks/{id}/test

GET /api/v1/webhooks/events returns the live catalog. An endpoint's events list accepts exact names, class wildcards such as site.*, or *. An empty list subscribes to every event except webhook.ping.

Via the panel: Settings -> Developers.

Via the API:

Terminal window
curl -fsSL -X POST \
-H "Authorization: Bearer hcp_live_..." \
-H "Content-Type: application/json" \
https://panel.example.com:2087/api/v1/webhooks \
-d '{
"url": "https://my-ci.example.com/hostcraft",
"events": ["site.*", "account.created"],
"description": "CI"
}'

Omit secret and the daemon generates one (whsec_...). The response (201) contains the secret exactly once; later reads only show its last four characters. Secrets are stored AES-GCM encrypted.

Method and path Purpose
GET /api/v1/webhooks List endpoints
POST /api/v1/webhooks Create (url, events, description, secret, active)
GET, PATCH, DELETE /api/v1/webhooks/{id} Read, update, delete. PATCH accepts rotate_secret: true
POST /api/v1/webhooks/{id}/test Send a webhook.ping synchronously
GET /api/v1/webhooks/{id}/deliveries Delivery log, newest first
GET /api/v1/webhooks/{id}/deliveries/{delivery} One delivery including its payload
POST /api/v1/webhooks/{id}/deliveries/{delivery}/redeliver Queue a delivery again
GET /api/v1/webhooks/hooks Installed hook scripts and whether each is runnable

Every delivery is a POST to the configured URL with:

  • Content-Type: application/json
  • X-Hostcraft-Event: <event> (for example site.created)
  • X-Hostcraft-Delivery: <uuid>
  • X-Hostcraft-Signature: t=<unix-ts>,v1=<hex-hmac-sha256>

The body is:

{
"id": "6f0c...",
"event": "account.created",
"created_at": "2026-10-06T01:02:03Z",
"data": {
"run_id": "...",
"workflow": "account.create",
"phase": "succeeded",
"account_id": "...",
"username": "bob"
}
}

data carries the workflow run id and an allow-listed set of identifiers from the operation (username, domain, FQDN, ids). Passwords, keys and environment values are never included. Failed runs add error.

The HMAC is computed over <unix-ts>.<request-body> using the shared secret. Redirects are not followed. A 2xx response is success; anything else is retried after 30s, 1m, 5m, 30m, 2h and 12h (one attempt plus six retries, then the delivery is marked failed). Pending deliveries survive a daemon restart. Delivery history is kept for 30 days.

Always verify the signature server-side. Never trust a webhook payload that fails signature verification.

import hmac, hashlib, time
def verify(secret: str, signature_header: str, body: bytes, max_age: int = 300) -> bool:
parts = dict(p.split('=', 1) for p in signature_header.split(','))
ts = int(parts['t'])
if abs(time.time() - ts) > max_age:
return False # replay attack or clock skew
expected = hmac.new(
secret.encode(),
f"{ts}.".encode() + body,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(parts['v1'], expected)

Hook scripts are local executables that HostCraft invokes at lifecycle points on the server itself. Use hooks when you need to act inside the same machine rather than over HTTP.

/etc/hostcraft/hooks/
account.created.d/
01-billing-provision (executable)
site.created.d/
01-cdn-purge
02-smoke-test
mail.dkim.rotated.d/
backup.completed.d/

The installer creates one .d directory per hook point. Files in each directory run as root in lexical order (run-parts semantics: names may only contain letters, digits, _ and -, so foo.bak or foo~ are ignored). Each script gets a 60 second timeout, the event JSON on stdin and HOSTCRAFT_HOOK=<hook point> in its environment. Scripts that are not owned by root or are group or world writable are refused. Every run (and every refusal) is written to the audit log as action hook.run with the exit code. A non-zero exit never blocks the operation; if you need to gate an operation, use a webhook instead. Settings -> Developers lists the installed scripts.

The hook point is the event name, except for these documented aliases:

Event Hook point
mail.dkim_rotated mail.dkim.rotated
backup.run_completed backup.completed
backup.run_failed backup.failed

webhook.ping has no hook point. Each hook receives the same JSON body a webhook delivery would.

/etc/hostcraft/hooks/site.created.d/01-cdn-purge
#!/bin/sh
set -e
DOMAIN="$(jq -r '.data.fqdn // .data.domain')"
curl -fsSL -X POST \
"https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/purge_cache" \
-H "Authorization: Bearer ${CF_TOKEN}" \
-d "{\"hosts\":[\"${DOMAIN}\"]}"

There are no first-party SDKs or OpenAPI export yet. Call the REST API with any HTTP client, or generate a gRPC client from the protos; the conventions above (bearer tokens, error envelope, status mapping) are the full contract.