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 indaemon/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.
Authentication
Section titled “Authentication”Session cookies
Section titled “Session cookies”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.
# Authenticate and save the session cookiecurl -fsSL -c cookies.txt -X POST \ https://panel.example.com/api/v1/auth/login \ -H "Content-Type: application/json" \
# Use the saved cookiecurl -fsSL -b cookies.txt \ https://panel.example.com/api/v1/sitesAPI tokens
Section titled “API tokens”API tokens are the preferred authentication method for programmatic access. Create a token under Settings -> Developers -> API tokens, or with a signed-in session:
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:
curl -fsSL \ -H "Authorization: Bearer hcp_live_..." \ -H "Accept: application/json" \ https://panel.example.com/api/v1/sitesThe 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.
API base URLs
Section titled “API base URLs”| 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) |
REST conventions
Section titled “REST conventions”Request and response format
Section titled “Request and response format”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) |
Error envelope
Section titled “Error envelope”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.
Long-running operations
Section titled “Long-running operations”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.
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.
Rate limits
Section titled “Rate limits”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.
API versioning
Section titled “API versioning”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.
Transport
Section titled “Transport”| 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.
Authentication
Section titled “Authentication”- Local root. A client running as uid 0 on the Unix socket is identified by the kernel (
SO_PEERCRED) and acts as asuper_adminnamedroot. Root also satisfies the re-authentication step that destructive calls demand of browser sessions, so it can delete accounts and manage admin users.GetMereports it as userroot. - API token. Every other caller (a member of
hostcraft-adminon the socket, or any caller ongrpc_addr) must sendauthorization: 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.
Authorization and audit
Section titled “Authorization and audit”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.
Errors
Section titled “Errors”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 |
Field names
Section titled “Field names”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.
Streaming RPCs
Section titled “Streaming RPCs”| 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 |
Examples with grpcurl
Section titled “Examples with grpcurl”SOCK=/run/hostcraft/hostcraftd.sock
# As root on the server: list services, create and read an accountsudo grpcurl -plaintext -unix $SOCK listsudo grpcurl -plaintext -unix $SOCK hostcraft.v1.AccountService/CreateAccount \sudo grpcurl -plaintext -unix $SOCK hostcraft.v1.AccountService/ListAccountssudo grpcurl -plaintext -unix $SOCK hostcraft.v1.WorkflowService/StreamWorkflow -d '{"id":"<workflow_id>"}'
# As a member of hostcraft-admin, with an API tokengrpcurl -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
Section titled “Webhooks”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 classes
Section titled “Event classes”| 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.
Registering a webhook
Section titled “Registering a webhook”Via the panel: Settings -> Developers.
Via the API:
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 |
Webhook delivery
Section titled “Webhook delivery”Every delivery is a POST to the configured URL with:
Content-Type: application/jsonX-Hostcraft-Event: <event>(for examplesite.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.
Verifying a delivery
Section titled “Verifying a delivery”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
Section titled “Hook scripts”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.
Layout
Section titled “Layout”/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.
Supported hook points
Section titled “Supported hook points”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.
#!/bin/shset -eDOMAIN="$(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.