Status & Monitoring
Section titled “Status & Monitoring”The daemon exposes five HTTP endpoints that describe its own health and the runtime state of every program it supervises: an unauthenticated health probe, two program-status readouts (one for the whole kit, one for a single program), and two log readers (a one-shot snapshot, and a live SSE tail). Every route is served from the daemon’s per-container hostname and is reached only through the kit’s URL; requests that do not arrive through the URL are refused by the Source IP Guard before the handler runs.
Health
Section titled “Health”GET /api/v1/daemon/health
Section titled “GET /api/v1/daemon/health”Service health check. Returns the standardized JSON health response when the daemon is up. ingressReplyRoute and the sandbox block report conditions the daemon cannot repair by itself, but neither one flips status because the daemon is serving either way. Unauthenticated, but still subject to the Source IP Guard.
This endpoint takes no parameters.
Example requests
Section titled “Example requests”curl https://{projectId}-{containerId}-daemon-1.node-example-1.containers.hoody.com/api/v1/daemon/healthimport { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-daemon-1.node-example-1.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.daemon.kit.getHealth();hoody daemon healthExample responses
Section titled “Example responses”Service is healthy.
status is the gate: "ok" means the daemon is up and serving. Sandbox and ingress problems are surfaced as separate fields rather than by flipping status.
{ "status": "ok", "service": "kit-daemon", "built": "2026-02-10T12:00:00Z", "started": "2026-02-10T17:39:50Z", "memory": { "rss": 25165824, "heap": null }, "fds": 14, "pid": 1234, "ingressReplyRoute": "healthy", "ip": "203.0.113.42", "userAgent": null, "sandbox": { "bwrap": true, "nft": true, "systemd_run": true, "landlock_abi": 9, "bwrap_version": "0.12.0", "bwrap_tmpfs_size": true, "firewall": "ok" }}| Field | Type | Required | Description |
|---|---|---|---|
status | string | Yes | Health status. |
service | string | Yes | Service name. |
started | string | Yes | Process start time as RFC3339 string. |
pid | integer | Yes | Process ID. |
ip | string | Yes | Container IP. |
built | string | No | Executable mtime as RFC3339 string. |
memory | object | null | No | Memory breakdown (rss in bytes, heap in bytes or null for Rust). |
fds | integer | No | Count of open file descriptors. |
ingressReplyRoute | string | No | State of the container’s ingress reply routing: healthy, degraded, unverified, or not_installed. degraded means published services for this container are unreachable because a VPN’s routing rules outrank the daemon’s; it is not self-repairable, and it is reported here rather than by flipping status. |
userAgent | string | No | The user agent that made this request, when applicable. |
sandbox | object | No | Availability of the mechanisms the program sandbox needs, probed once and cached. Sandbox problems do not change this endpoint’s HTTP status or its top-level status. |
sandbox.bwrap | boolean | No | Whether the bubblewrap binary is present and executable. |
sandbox.nft | boolean | No | Whether /usr/sbin/nft is present and executable (needed by every ingress policy). |
sandbox.systemd_run | boolean | No | Whether /usr/bin/systemd-run is present and executable. |
sandbox.landlock_abi | integer | No | Landlock ABI level reported by the kernel; 0 when Landlock is unavailable. TCP bind/connect restriction (network.mode: restricted) needs 4 or higher. |
sandbox.bwrap_version | string | No | Version reported by the bubblewrap binary, or null when it cannot be read. |
sandbox.bwrap_tmpfs_size | boolean | No | Whether that bubblewrap has --size (0.10.0 or newer), required by process.tmp_size. |
sandbox.firewall | string | No | Container-wide state of the ingress table: ok, degraded, or none. |
Refused by the Source IP Guard: Hoody Kit programs are reached through their URLs only. Answered before the handler, as plain text, not the JSON envelope. Applies to every route on this API.
Program status
Section titled “Program status”GET /api/v1/daemon/status
Section titled “GET /api/v1/daemon/status”Get all program statuses. Retrieves the current runtime status of every configured program: whether each one is running, stopped, or in another state, along with PID and uptime for the ones that are up. A port-range program is reported running whenever any instance is in a state other than stopped, including backoff or fatal. A status read whose supervisorctl call fails can report stopped or no instances, which is not proof the process ended.
This endpoint takes no parameters.
Example requests
Section titled “Example requests”curl https://{projectId}-{containerId}-daemon-1.node-example-1.containers.hoody.com/api/v1/daemon/statusimport { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-daemon-1.node-example-1.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.daemon.programs.listStatus();hoody daemon statusExample responses
Section titled “Example responses”Successfully retrieved status for all programs.
Programs in various states:
{ "success": true, "statuses": [ { "id": 1, "name": "web-server", "enabled": true, "status": { "id": 1, "status": "running", "pid": 1234, "uptime": "2:15:30" } }, { "id": 2, "name": "nodejs-app", "enabled": false, "status": { "id": 2, "status": "stopped" } }, { "id": 3, "name": "worker", "enabled": true, "status": { "id": 3, "status": "backoff" } } ]}All programs running:
{ "success": true, "statuses": [ { "id": 1, "name": "web-server", "enabled": true, "status": { "id": 1, "status": "running", "pid": 1234, "uptime": "5:30:15" } }, { "id": 2, "name": "api-server", "enabled": true, "status": { "id": 2, "status": "running", "pid": 1235, "uptime": "5:30:10" } } ]}| Field | Type | Required | Description |
|---|---|---|---|
success | boolean | Yes | Whether the read succeeded. |
statuses | array | Yes | Status information for all programs. |
statuses[].id | integer | Yes | Program identifier. |
statuses[].name | string | Yes | Program name. |
statuses[].enabled | boolean | Yes | Whether the program is enabled. |
statuses[].status | object | Yes | Runtime status for the program. |
statuses[].status.id | integer | Yes | Program identifier (mirrors statuses[].id). |
statuses[].status.status | string | Yes | Current runtime status of the program: running, stopped, starting, stopping, backoff, exited, fatal, or unknown. |
statuses[].status.pid | integer | No | Process ID when running; omitted otherwise. |
statuses[].status.uptime | string | No | Uptime as supervisord renders it. Omitted when the program is not running. |
statuses[].status.supervisor_state | string | No | supervisord’s own state, present when it differs from status (e.g. a run has ended but the launcher is still finishing). |
statuses[].status.last_run | object | No | The last ended run. Absent before the first run ends. |
statuses[].status.last_run.exit_code | integer | Yes | The program’s exit code, or null when ended by a signal. |
statuses[].status.last_run.signal | string | Yes | Name of the signal that ended the program, or null. |
statuses[].status.last_run.error | string | Yes | Why the program never ran, the daemon’s reason (ttl expired, daemon restarted before the program finished), or null. |
statuses[].status.last_run.ended_at | integer | Yes | When the run ended, in milliseconds since the Unix epoch. |
statuses[].status.last_run.final | boolean | Yes | true when no automatic restart or retry follows. |
statuses[].status.last_run.source | string | No | Where the result came from: launcher or supervisor. |
GET /api/v1/daemon/status/{id}
Section titled “GET /api/v1/daemon/status/{id}”Get specific program status. Retrieves the runtime status of one program. A standard program returns status (with PID and uptime when running). A port-range program returns instance when port is given, otherwise instances with running_count and total_count. The instances list omits stopped entries and includes every other state, so running_count counts backoff, exited, and fatal instances too; check each instance’s own status for liveness. A status read whose supervisorctl call fails can report stopped or an empty list, which is not proof the process ended.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | integer | Yes | Unique numeric identifier of the program. An id that is not a valid i32 (a word, an empty segment, a number too large) is refused by the path extractor with a plain-text 404 on every route that takes an id, before the handler runs. An id that parses but names no program is handled by the route, not the extractor. |
port | query | integer | No | Filter to a specific port instance (for port-range programs only). |
include_stats | query | string | No | Include resource stats (CPU, memory, process tree) for running programs. Accepts the literal strings "true" and "false". Where the stats land depends on the program: standard programs get a top-level stats; port-range programs get one stats per instance, on the instance itself (instance.stats or instances[].stats), never at the top level. Each carries pid, started_at, cpu_percent, memory_rss_bytes, process_count, and a per-process breakdown. |
Example requests
Section titled “Example requests”curl "https://{projectId}-{containerId}-daemon-1.node-example-1.containers.hoody.com/api/v1/daemon/status/100"import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-daemon-1.node-example-1.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.daemon.programs.getStatus(100);hoody daemon programs status --id 100Example responses
Section titled “Example responses”Successfully retrieved program status.
The shape returned depends on the program: standard programs carry status; a port-range program queried with a port carries instance; a port-range program queried without one carries instances plus running_count/total_count. Only success is common to all three, which is why it is the sole required field.
Running
{ "success": true, "status": { "id": 1, "status": "running", "pid": 1234, "uptime": "2:15:30" }}Stopped
{ "success": true, "status": { "id": 1, "status": "stopped" }}Fatal
{ "success": true, "status": { "id": 1, "status": "fatal" }}| Field | Type | Required | Description |
|---|---|---|---|
success | boolean | Yes | Whether the read succeeded. |
status | object | No | Runtime status for a standard (non-port-range) program. |
status.id | integer | Yes | Program identifier. |
status.status | string | Yes | Current runtime status of the program. |
status.pid | integer | No | Process ID when running; omitted otherwise. |
status.uptime | string | No | Uptime as supervisord renders it. |
status.supervisor_state | string | No | supervisord’s own state, present when it differs from status. |
status.last_run | object | No | The last ended run. Absent before the first run ends. |
status.last_run.exit_code | integer | Yes | The program’s exit code, or null. |
status.last_run.signal | string | Yes | Name of the signal that ended the program, or null. |
status.last_run.error | string | Yes | Why the program never ran, the daemon’s reason, or null. |
status.last_run.ended_at | integer | Yes | When the run ended. |
status.last_run.final | boolean | Yes | true when no automatic restart or retry follows. |
status.last_run.source | string | No | Source of the result (launcher or supervisor). |
stats | object | No | Resource stats for the program’s process tree (standard programs only). Present when include_stats=true and the program is running. |
stats.pid | integer | Yes | Root process ID (the PID supervisord manages). |
stats.started_at | string | Yes | ISO 8601 timestamp of when the process started. |
stats.cpu_percent | number | Yes | Total CPU usage across all processes in the tree. A lifetime average, not instantaneous. |
stats.memory_rss_bytes | integer | Yes | Total RSS in bytes across all processes in the tree. |
stats.process_count | integer | Yes | Number of processes in the tree (root + children). |
stats.processes | array | Yes | Per-process breakdown of the process tree. |
stats.processes[].pid | integer | Yes | Process ID. |
stats.processes[].command | string | Yes | Command line of the process. |
stats.processes[].cpu_percent | number | Yes | CPU usage for this process (a lifetime average). |
stats.processes[].memory_rss_bytes | integer | Yes | RSS for this process. |
instance | object | No | Port-range program, single-port query. |
instance.port | integer | Yes | Port number this instance is running on. |
instance.instance_name | string | Yes | Supervisord process name (format: programname_port). |
instance.status | string | Yes | Current runtime status of this instance. |
instance.pid | integer | No | Process ID when running; omitted otherwise. |
instance.uptime | string | No | Uptime as supervisord renders it. |
instance.supervisor_state | string | No | supervisord’s own state, present when it differs from status. |
instance.last_run | object | No | The last ended run. |
instance.last_run.exit_code | integer | Yes | Exit code, or null. |
instance.last_run.signal | string | Yes | Signal name, or null. |
instance.last_run.error | string | Yes | Reason, or null. |
instance.last_run.ended_at | integer | Yes | When the run ended. |
instance.last_run.final | boolean | Yes | true when no automatic restart or retry follows. |
instance.last_run.source | string | No | Source of the result. |
instance.stats | object | No | Resource stats for this instance’s process tree. Present when include_stats=true and the instance is running. |
instance.stats.pid | integer | Yes | Root process ID. |
instance.stats.started_at | string | Yes | ISO 8601 start timestamp. |
instance.stats.cpu_percent | number | Yes | CPU usage. |
instance.stats.memory_rss_bytes | integer | Yes | RSS in bytes. |
instance.stats.process_count | integer | Yes | Number of processes. |
instance.stats.processes | array | Yes | Per-process breakdown. |
instance.stats.processes[].pid | integer | Yes | Process ID. |
instance.stats.processes[].command | string | Yes | Command line. |
instance.stats.processes[].cpu_percent | number | Yes | CPU usage. |
instance.stats.processes[].memory_rss_bytes | integer | Yes | RSS in bytes. |
instances | array | No | Port-range program, no port given: one entry per non-stopped instance. |
instances[].port | integer | Yes | Port number this instance is running on. |
instances[].instance_name | string | Yes | Supervisord process name. |
instances[].status | string | Yes | Current runtime status of this instance. |
instances[].pid | integer | No | Process ID when running; omitted otherwise. |
instances[].uptime | string | No | Uptime as supervisord renders it. |
instances[].supervisor_state | string | No | supervisord’s own state. |
instances[].last_run | object | No | The last ended run. |
instances[].last_run.exit_code | integer | Yes | Exit code, or null. |
instances[].last_run.signal | string | Yes | Signal name, or null. |
instances[].last_run.error | string | Yes | Reason, or null. |
instances[].last_run.ended_at | integer | Yes | When the run ended. |
instances[].last_run.final | boolean | Yes | true when no automatic restart or retry follows. |
instances[].last_run.source | string | No | Source of the result. |
instances[].stats | object | No | Resource stats for this instance’s tree. |
instances[].stats.pid | integer | Yes | Root process ID. |
instances[].stats.started_at | string | Yes | ISO 8601 start timestamp. |
instances[].stats.cpu_percent | number | Yes | CPU usage. |
instances[].stats.memory_rss_bytes | integer | Yes | RSS in bytes. |
instances[].stats.process_count | integer | Yes | Number of processes. |
instances[].stats.processes | array | Yes | Per-process breakdown. |
instances[].stats.processes[].pid | integer | Yes | Process ID. |
instances[].stats.processes[].command | string | Yes | Command line. |
instances[].stats.processes[].cpu_percent | number | Yes | CPU usage. |
instances[].stats.processes[].memory_rss_bytes | integer | Yes | RSS in bytes. |
running_count | integer | No | Port-range programs: number of instances that are starting or running. instances lists every non-stopped instance, so it can be longer. |
total_count | integer | No | Port-range programs: total number of possible instances. |
No program has that id, or the request was refused. An unknown id is refused with a 400 (not a 404): the response carries success: false and error naming the missing id. The same status is returned when a query parameter was present but unparseable (port, include_stats). These are rejected rather than silently ignored, because a dropped value would widen the operation to its default (an invalid port would return every instance instead of the one named; an invalid include_stats would return the unfiltered response).
{ "success": false, "error": "Program with ID 999 not found", "code": "INVALID_QUERY"}| Field | Type | Required | Description |
|---|---|---|---|
success | boolean | Yes | Always false for error responses. |
error | string | Yes | Human-readable error message. |
code | string | No | Machine-readable error code, present on some errors. INVALID_QUERY means a list route was given a query parameter it does not accept. |
GET /api/v1/daemon/programs/{id}/logs
Section titled “GET /api/v1/daemon/programs/{id}/logs”Get program logs. Retrieve the last N lines from a program’s stdout or stderr log file. For a file larger than 10 MiB, reading starts 10 MiB before the end and continues to the end, so a file that is still growing can yield more, so fewer lines than requested can come back, and the first line can be partial; a cut inside a multi-byte UTF-8 character makes the read a 400. A successful lines=0 read returns empty content. Disabled logging, a missing port on a port-range program, a log path outside the log root, and a missing, unreadable, or non-UTF-8 file are 400s even when the program exists; a file error carries type and log_file too.
This is a snapshot of the file’s end. To keep receiving lines as they are written, use the SSE endpoint below.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | integer | Yes | Program ID. |
type | query | string | No | Log stream: stdout or stderr. Default: stdout. |
lines | query | integer | No | Number of lines to return from the end of the file. Default: 100. A non-negative integer: 0 returns empty content, and the server clamps values above 10000 to 10000. |
port | query | integer | No | Port number (required for port-range programs). |
Example requests
Section titled “Example requests”curl "https://{projectId}-{containerId}-daemon-1.node-example-1.containers.hoody.com/api/v1/daemon/programs/100/logs?type=stdout&lines=100"import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-daemon-1.node-example-1.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.daemon.programs.getLogs(100, { type: 'stdout', lines: 100 });hoody daemon programs logs --id 100 --type stdout --lines 100Example responses
Section titled “Example responses”Log content retrieved successfully.
{ "success": true, "logs": "Server listening on :8080\nReady for connections\n", "type": "stdout", "lines": 2, "log_file": "/var/log/kit/programs/web-server.stdout.log"}| Field | Type | Required | Description |
|---|---|---|---|
success | boolean | No | Whether the read succeeded. |
error | string | No | Error message, or null. |
logs | string | No | Log content (last N lines). |
type | string | No | Log type that was read. |
lines | integer | No | Number of lines returned. |
log_file | string | No | Path to the log file that was read. |
The request was refused. A JSON {success: false, error} refusal, which for a file error also carries type and log_file. A malformed lines value is refused by the query extractor as plain text; a malformed port is a JSON 400.
When the response is application/json:
{ "success": false, "error": "Failed to read log file: No such file or directory", "type": "stderr", "log_file": "/var/log/kit/programs/web-server.stderr.log"}| Field | Type | Required | Description |
|---|---|---|---|
success | boolean | Yes | Always false. |
error | string | Yes | Human-readable error message. |
type | string | No | Present on a file error. |
log_file | string | No | Present on a file error: the path that was read. |
When the response is text/plain, the body is a plain string. One such body is Query deserialize error: invalid digit found in string.
GET /api/v1/daemon/programs/{id}/logs/stream
Section titled “GET /api/v1/daemon/programs/{id}/logs/stream”Follow program logs (SSE). Live tail of a program’s stdout or stderr log over Server-Sent Events: first the last lines complete lines, then every line appended afterwards, until the client disconnects. Program, type, and port select the file exactly as in getProgramLogs, and the same requests are refused with the same JSON 400s before the stream starts. The file is re-examined four times a second.
At most 32 log streams are open across the daemon at once; the next call past that is a 429.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | integer | Yes | Program ID. |
type | query | string | No | Log stream: stdout or stderr. Default: stdout. |
lines | query | integer | No | Number of complete lines replayed from the end of the file before following. Default: 100. 0 replays nothing and only follows; values above 10000 are clamped to 10000. The replay is found in the last 10 MiB of the file. Ignored when resuming with offset or Last-Event-ID. |
port | query | integer | No | Port number (required for port-range programs). |
offset | query | integer | No | Resume cursor: the id of the last line event received (a byte offset just past that line). Streaming restarts at the next line and no backlog is replayed. An offset beyond the end of the current file means the file was truncated or replaced since: the stream sends a reset event and starts from the top of the file. The cursor is a byte position, not a file identity, so a file replaced by one at least as long is not detected across a reconnect. Takes precedence over Last-Event-ID. |
Last-Event-ID | header | string | No | Same cursor as offset, sent automatically by an EventSource when it reconnects. Must be a non-negative integer. |
Example requests
Section titled “Example requests”curl -N "https://{projectId}-{containerId}-daemon-1.node-example-1.containers.hoody.com/api/v1/daemon/programs/100/logs/stream?type=stdout&lines=100"import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-daemon-1.node-example-1.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.daemon.programs.streamLogs(100, { type: 'stdout', lines: 100 });hoody daemon programs logs stream --id 100 --type stdoutExample responses
Section titled “Example responses”The stream is open. The body is text/event-stream and ends only when the client disconnects or after an error event. The Cache-Control response header carries no-cache.
event: opendata: {"log_file":"/var/log/kit/programs/web-server.stdout.log","type":"stdout","offset":2048,"exists":true}
event: lineid: 2102data: {"line":"Server listening on :8080","offset":2102}
event: lineid: 2146data: {"line":"Ready for connections","offset":2146}
: keepalive
event: lineid: 2196data: {"line":"Accepted connection from 203.0.113.42","offset":2196}| Field | Type | Required | Description |
|---|---|---|---|
line | string | No | One log line, without its terminator. Present on line. |
offset | integer | No | Byte offset: just past the line (line), where following starts (open), or 0 (reset). |
reason | string | No | Why reading restarted (reset): rotated or truncated. |
log_file | string | No | The file being followed. Present on open. |
type | string | No | The log stream (stdout or stderr). Present on open. |
exists | boolean | No | Whether the file existed when the stream opened. Present on open. |
error | string | No | Why the stream ended. Present on error. |
The request was refused. A JSON {success: false, error} refusal, sent instead of a stream: unknown program, disabled logging, a missing port on a port-range program, an invalid type, port, offset, or Last-Event-ID, or a log path outside the log root. A malformed lines value is refused by the query extractor as plain text.
When the response is application/json:
{ "success": false, "error": "Logging is disabled for this program", "type": "stderr", "log_file": "/var/log/kit/programs/disabled.stdout.log"}| Field | Type | Required | Description |
|---|---|---|---|
success | boolean | Yes | Always false. |
error | string | Yes | Human-readable error message. |
type | string | No | Present on a file error. |
log_file | string | No | Present on a file error: the path that was read. |
When the response is text/plain, the body is a plain string. One such body is Query deserialize error: invalid digit found in string.
Too many log streams are open (the daemon-wide cap is 32).
{ "success": false, "error": "Too many log streams open (limit 32)"}| Field | Type | Required | Description |
|---|---|---|---|
success | boolean | Yes | Always false. |
error | string | Yes | Human-readable error message. |
code | string | No | Machine-readable error code, when the server provides one. |