Terminal: System monitoring
Section titled “Terminal: System monitoring”This page documents the system monitoring endpoints exposed by the terminal container: aggregate resource statistics, process inventory, network port discovery, display state, daemon configuration, and the power management controls (reboot, shutdown, and process freeze or unfreeze). It also covers the optional client diagnostics beacon that the terminal client posts so the server can correlate render or connection issues with server-side state.
All endpoints below share the same base URL, since they are all served by the terminal container.
Health check
Section titled “Health check”GET /api/v1/terminal/health
Section titled “GET /api/v1/terminal/health”Unauthenticated health probe for the terminal container. Always responds with HTTP 200 and a JSON body when the service is up.
This endpoint takes no parameters.
curl -X GET 'https://{projectId}-{containerId}-terminal-1.{server}.containers.hoody.com/api/v1/terminal/health'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-terminal-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.terminal.kit.getHealth();Response
Section titled “Response”{ "status": "ok", "service": "hoody-terminal", "built": "2026-04-14T10:00:00Z", "started": "2026-04-14T10:01:00Z", "memory": { "rss": 52428800, "heap": null }, "fds": 47, "pid": 1234, "ip": "198.51.100.42", "userAgent": "curl/8.0.0"}System resources
Section titled “System resources”GET /api/v1/system/resources
Section titled “GET /api/v1/system/resources”Returns aggregate CPU, memory, network interface, uptime, and disk statistics for the container.
This endpoint takes no parameters.
curl -X GET 'https://{projectId}-{containerId}-terminal-1.{server}.containers.hoody.com/api/v1/system/resources'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-terminal-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.terminal.system.getStats();Response
Section titled “Response”{ "memory": { "total_kb": 16777216, "free_kb": 2097152, "available_kb": 8388608, "buffers_kb": 1468, "cached_kb": 6291456, "swap_total_kb": 4194304, "swap_free_kb": 4194304, "swap_cached_kb": 0, "used_kb": 8388608, "used_percent": 50 }, "cpu": { "usage_percent": 15.5, "load_1min": 1.2, "load_5min": 0.9, "load_15min": 0.7, "user": 1044524, "nice": 4657, "system": 467452, "idle": 33880505, "iowait": 25169 }, "uptime": { "uptime_seconds": 86400.5, "idle_seconds": 675000.2 }, "network": [ { "name": "eth0", "rx_bytes": 1024000, "rx_packets": 8000, "rx_errors": 0, "tx_bytes": 2048000, "tx_packets": 9000, "tx_errors": 0 } ], "disk": [ { "device": "nvme0n1", "reads": 96886, "writes": 77549, "read_sectors": 4942803, "write_sectors": 1625191 } ]}Processes
Section titled “Processes”GET /api/v1/system/processes
Section titled “GET /api/v1/system/processes”Lists every process running in the container with CPU, memory, and state information. Supports filtering by name or command line, sorting by common metrics, and limiting the number of results. cpu_percent is the process’s CPU time over its lifetime (above 100 on several cores); mem_percent is resident memory over total memory; vsize and rss are bytes; output_type is what the process’s stdout is (pty, pipe, null, file, other, or unknown when it cannot be read). total_processes equals process_count: both count the processes returned.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
sort | query | string | No | Sort by field: cpu, memory, pid, name (default: pid) |
limit | query | integer | No | Maximum number of processes to return (default: 1000) |
filter | query | string | No | Keep processes whose name or command line contains this text (case-sensitive) |
curl -X GET 'https://{projectId}-{containerId}-terminal-1.{server}.containers.hoody.com/api/v1/system/processes?sort=cpu&limit=20'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-terminal-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.terminal.processes.list({ sort: 'cpu', limit: 20 });Response
Section titled “Response”{ "process_count": 1, "total_processes": 1, "processes": [ { "pid": 1, "name": "systemd", "cmdline": "/sbin/init", "state": "S", "ppid": 0, "user": "root", "threads": 1, "cpu_percent": 0.07, "mem_percent": 0.01, "vsize": 25497600, "rss": 11001856, "priority": 20, "nice": 0, "output_type": "unknown" } ]}GET /api/v1/system/processes/{pid}
Section titled “GET /api/v1/system/processes/{pid}”Returns detailed information about a single process: the fields of the process list plus process group, session, controlling terminal, uid, CPU times and start time (clock ticks), the pty path (pty, only when the process has one) and its environment (env, only when readable).
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
pid | path | integer | Yes | Process ID |
curl -X GET 'https://{projectId}-{containerId}-terminal-1.{server}.containers.hoody.com/api/v1/system/processes/1234'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-terminal-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.terminal.processes.get(1234);Response
Section titled “Response”{ "pid": 1234, "name": "bash", "cmdline": "bash", "state": "S", "ppid": 1200, "pgrp": 1234, "session": 1234, "tty_nr": 34816, "user": "user", "uid": 1000, "threads": 1, "priority": 20, "nice": 0, "cpu_percent": 0.2, "mem_percent": 0.01, "vsize": 9469952, "rss": 5357568, "utime": 12, "stime": 4, "starttime": 2268334, "output_type": "pty", "pty": "/dev/pts/0", "env": { "PATH": "/usr/bin", "TERM": "xterm-256color" }}{ "error": "Process not found"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
PROCESS_NOT_FOUND | Process does not exist | Check PID is valid and process is running | Check PID is valid and process is running |
POST /api/v1/system/process/signal
Section titled “POST /api/v1/system/process/signal”Sends a Unix signal to one or more processes. Target by PID for a single process, or by name to signal every matching process. The string form of signal accepts the conventional names with or without the SIG prefix; the integer form accepts any value in [0, NSIG), including realtime signals SIGRTMIN..SIGRTMAX (typically 34..64 on Linux), which have no portable string names.
This endpoint takes no parameters.
Request body
Section titled “Request body”| Field | Type | Required | Description |
|---|---|---|---|
pid | integer | No | Process ID to signal. Mutually exclusive with name. |
name | string | No | Process name to signal. Signals every matching process. Mutually exclusive with pid. |
signal | string or integer | No | Signal to send. String form accepts SIGTERM, TERM, 15, etc. (with or without SIG prefix). Integer form accepts any value in [0, NSIG), including realtime signals SIGRTMIN..SIGRTMAX (typically 34..64 on Linux), which have no portable string names. |
force | boolean | No | Shorthand for SIGKILL (true) or SIGTERM (false). Overrides the signal parameter. |
curl -X POST 'https://{projectId}-{containerId}-terminal-1.{server}.containers.hoody.com/api/v1/system/process/signal' \ -H 'Content-Type: application/json' \ -d '{"pid": 1234, "signal": "SIGTERM"}'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-terminal-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.terminal.processes.signal({ pid: 1234, signal: 'SIGTERM' });Response
Section titled “Response”{ "success": true, "message": "Signal SIGTERM sent to PID 1234", "affected_pids": [1234]}{ "error": "Must specify either pid or name"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
MISSING_TARGET | Must specify pid or name | Provide either pid or name parameter | Provide either pid or name parameter |
INVALID_SIGNAL | Invalid signal name | Use valid signal (SIGTERM, SIGKILL, etc.) | Use valid signal (SIGTERM, SIGKILL, etc.) |
{ "error": "Permission denied: EPERM"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
PERMISSION_DENIED | No permission to signal process | Check process ownership | Check process ownership |
{ "error": "method_not_allowed", "allow": "POST"}{ "error": "Failed to send signal"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
SIGNAL_FAILED | Failed to send signal | Check process exists | Check process exists |
Process freeze and unfreeze
Section titled “Process freeze and unfreeze”The freeze and unfreeze endpoints suspend and resume a process (or an entire process tree) by delivering SIGSTOP and SIGCONT. They are the recommended way to pause background workloads (builds, fine-tunes, AI inference) without losing in-memory state.
Both endpoints share the same request body. Exactly one of pid or name must be provided.
POST /api/v1/system/processes/freeze
Section titled “POST /api/v1/system/processes/freeze”Suspends execution of one or more processes by delivering SIGSTOP. The kernel halts every thread of the target’s thread group until a matching SIGCONT arrives (see the unfreeze endpoint below).
This endpoint takes no parameters.
Request body
Section titled “Request body”| Field | Type | Required | Description |
|---|---|---|---|
pid | integer | No | Process ID to freeze. Mutually exclusive with name. PIDs 1, 2, the server’s own PID, and the server’s parent PID are guarded and rejected with 403. |
name | string | No | Process name (case-insensitive comm match; freezes every matching process). Mutually exclusive with pid. Linux truncates comm to 15 characters, so longer names silently match nothing. |
include_descendants | boolean | No | When true, also freezes every descendant via a one-shot /proc PPID snapshot (bounded at 65535 PIDs). Default false. The parent is signalled before descendants to shrink the fork or escape race window, but the operation is best-effort, not atomic. With descendants, by-name dedupes overlapping subtrees so the same PID is not signalled twice. |
curl -X POST 'https://{projectId}-{containerId}-terminal-1.{server}.containers.hoody.com/api/v1/system/processes/freeze' \ -H 'Content-Type: application/json' \ -d '{"pid": 1234, "include_descendants": true}'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-terminal-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.terminal.processes.pause({ pid: 1234, include_descendants: true });Response
Section titled “Response”{ "success": true, "operation": "freeze", "signal": "SIGSTOP", "affected_pids": [1234, 5678], "skipped": [], "frozen_count": 2, "message": "Froze 2 process(es)"}{ "error": "Must specify either 'pid' or 'name' (not both)"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
MISSING_TARGET | Must specify pid or name | Provide either pid or name parameter | Provide either pid or name parameter |
INVALID_PID | pid must be a positive integer within int range | pid must be a positive integer within int range | Contact support |
{ "error": "Permission denied: target PID is protected or not signalable by this UID"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
PERMISSION_DENIED | No permission to freeze process | Either the kernel returned EPERM (different UID) or the PID is in the guarded set (init/kthreadd/self/parent) | Either the kernel returned EPERM (different UID) or the PID is in the guarded set (init/kthreadd/self/parent) |
{ "error": "Process not found: ESRCH"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
PROCESS_NOT_FOUND | No process matching pid/name | No process matching pid/name | Contact support |
{ "error": "method_not_allowed", "allow": "POST"}{ "error": "Failed to freeze process"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
FREEZE_FAILED | Failed to freeze process | Check process exists | Check process exists |
PROC_SNAPSHOT_OOM | /proc snapshot exhausted memory; transient | /proc snapshot exhausted memory; transient | Contact support |
PROC_TABLE_TOO_LARGE | /proc PID table exceeds HT_PID_TABLE_MAX (65535) | /proc PID table exceeds HT_PID_TABLE_MAX (65535) | Contact support |
POST /api/v1/system/processes/unfreeze
Section titled “POST /api/v1/system/processes/unfreeze”Resumes execution of one or more previously stopped processes by delivering SIGCONT. The body schema is identical to freeze. Calling unfreeze on a process that is already running is harmless: SIGCONT is a no-op for processes that are not stopped.
This endpoint takes no parameters.
Request body
Section titled “Request body”| Field | Type | Required | Description |
|---|---|---|---|
pid | integer | No | Process ID to unfreeze. Mutually exclusive with name. The guarded-PID set (1, 2, self, parent) is the same as for freeze; calling unfreeze on a guarded PID returns 403. |
name | string | No | Process name (case-insensitive comm match). Mutually exclusive with pid. Linux truncates comm to 15 characters, so longer names silently match nothing. |
include_descendants | boolean | No | Also unfreeze every descendant via a /proc PPID snapshot (bounded at 65535 PIDs). Default false. By-name dedupes overlapping subtrees. |
curl -X POST 'https://{projectId}-{containerId}-terminal-1.{server}.containers.hoody.com/api/v1/system/processes/unfreeze' \ -H 'Content-Type: application/json' \ -d '{"pid": 1234, "include_descendants": true}'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-terminal-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.terminal.processes.resume({ pid: 1234, include_descendants: true });Response
Section titled “Response”{ "success": true, "operation": "unfreeze", "signal": "SIGCONT", "affected_pids": [1234, 5678], "skipped": [], "unfrozen_count": 2, "message": "Unfroze 2 process(es)"}{ "error": "Must specify either 'pid' or 'name' (not both)"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
MISSING_TARGET | Must specify pid or name | Provide either pid or name parameter | Provide either pid or name parameter |
INVALID_PID | pid must be a positive integer within int range | pid must be a positive integer within int range | Contact support |
{ "error": "Permission denied: target PID is protected or not signalable by this UID"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
PERMISSION_DENIED | No permission to unfreeze process | Either the kernel returned EPERM (different UID) or the PID is in the guarded set (init/kthreadd/self/parent) | Either the kernel returned EPERM (different UID) or the PID is in the guarded set (init/kthreadd/self/parent) |
{ "error": "Process not found: ESRCH"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
PROCESS_NOT_FOUND | No process matching pid/name | No process matching pid/name | Contact support |
{ "error": "method_not_allowed", "allow": "POST"}{ "error": "Failed to unfreeze process"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
UNFREEZE_FAILED | Failed to unfreeze process | Check process exists | Check process exists |
PROC_SNAPSHOT_OOM | /proc snapshot exhausted memory; transient | /proc snapshot exhausted memory; transient | Contact support |
PROC_TABLE_TOO_LARGE | /proc PID table exceeds HT_PID_TABLE_MAX (65535) | /proc PID table exceeds HT_PID_TABLE_MAX (65535) | Contact support |
Network ports
Section titled “Network ports”GET /api/v1/system/ports
Section titled “GET /api/v1/system/ports”Lists every listening TCP and UDP port on the container, with the owning PID, user, program, and command line. Use the query parameters to filter the result set; the endpoint does not paginate and returns the full matching set in one response.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
protocol | query | string | No | Filter by protocol: tcp, udp, or comma-separated list |
user | query | string | No | Filter by user (exact match) |
port | query | integer | No | Filter by specific port number |
ip | query | string | No | Filter by IP address (comma-separated list) |
skip_program | query | string | No | Exclude specific programs (comma-separated list) |
http_only | query | boolean | No | Only return HTTP services |
hoody_only | query | boolean | No | Only return Hoody Kit services |
curl -X GET 'https://{projectId}-{containerId}-terminal-1.{server}.containers.hoody.com/api/v1/system/ports?protocol=tcp&http_only=true'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-terminal-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.terminal.system.listPorts({ protocol: 'tcp', http_only: true });Response
Section titled “Response”[ { "protocol": "tcp", "port": 7681, "ip": "0.0.0.0", "pid": 1234, "user": "user", "program": "hoody-terminal", "cmdline": "./hoody-terminal -p 7681", "is_http": true, "hoody_kit": true }]Displays
Section titled “Displays”GET /api/v1/system/displays
Section titled “GET /api/v1/system/displays”Returns information about the displays connected to the container, as reported by the external display script. Each entry includes a display identifier, the current resolution, and the session name it is attached to. To stop a running display, see the stop endpoint below.
This endpoint takes no parameters.
curl -X GET 'https://{projectId}-{containerId}-terminal-1.{server}.containers.hoody.com/api/v1/system/displays'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-terminal-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.terminal.system.listDisplays();Response
Section titled “Response”[ { "display": "0", "resolution": "1920x1080", "session_name": "session1" }]POST /api/v1/system/displays/{display}/stop
Section titled “POST /api/v1/system/displays/{display}/stop”Stops the X display :{display} and everything drawing on it. A display started by a terminal session (the session’s display, with or without desktop/desktop_env) keeps running when that session is deleted, desktop environment included, so it stays usable from other sessions and from Hoody Display; this route is how it ends. The display’s session server receives SIGTERM, which shuts down its X server (a session server that exits while still starting up can leave its X server running; that X server then receives SIGTERM too). The window manager, panels, and applications on the display exit with it. A display without a session server has its X server signalled directly. Whatever is still running after 10 seconds receives SIGKILL (forced is true). The response is sent once the display is gone. Only displays the terminal service can reach are found, so an X server you started with -nolisten local is not found. Background services a desktop started outside the display (for example a D-Bus session bus) are not stopped. Sessions whose DISPLAY points at the stopped display keep running; the next session created with that display starts it again.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
display | path | integer | Yes | Display number (the N in :N), 0-65535 |
curl -X POST 'https://{projectId}-{containerId}-terminal-1.{server}.containers.hoody.com/api/v1/system/displays/1/stop'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-terminal-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.terminal.system.stopDisplay(1);Response
Section titled “Response”{ "display": 1, "stopped": true, "pid": 4321, "x_server_pid": 4330, "xpra": true, "forced": false, "message": "Display :1 stopped"}{ "error": "display must be a number from 0 to 65535", "code": "INVALID_DISPLAY"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
INVALID_DISPLAY | display must be a number from 0 to 65535 | display must be a number from 0 to 65535 | Contact support |
{ "error": "Cannot signal display :1: Operation not permitted", "code": "PERMISSION_DENIED"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
PERMISSION_DENIED | The display belongs to a user this server cannot signal (the server is not root), or it is served by a protected process | The display belongs to a user this server cannot signal (the server is not root), or it is served by a protected process | Contact support |
{ "error": "Display :1 is not running", "code": "DISPLAY_NOT_FOUND"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
DISPLAY_NOT_FOUND | No X server listens on the display’s abstract socket where this service runs (an X server started with -nolisten local, or in another network namespace, is not seen) | Nothing to stop | Nothing to stop |
{ "error": "method_not_allowed", "code": "METHOD_NOT_ALLOWED", "allow": "POST"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
METHOD_NOT_ALLOWED | Only POST method is accepted | Only POST method is accepted | Contact support |
{ "error": "Display :1 is still running after SIGKILL", "code": "DISPLAY_STOP_FAILED"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
DISPLAY_STOP_FAILED | The display’s processes were signalled but are still running, or its socket is still listening; or nothing was signalled because the system does not let the server pin a process before signalling it (pidfd_open is missing from an old kernel or blocked by a seccomp filter) | The display’s processes were signalled but are still running, or its socket is still listening; or nothing was signalled because the system does not let the server pin a process before signalling it (pidfd_open is missing from an old kernel or blocked by a seccomp filter) | Contact support |
DISPLAY_LOOKUP_FAILED | The server could not read the process table or the kernel’s list of listening sockets (unix_diag) to find the display | The server could not read the process table or the kernel’s list of listening sockets (unix_diag) to find the display | Contact support |
Daemon configuration
Section titled “Daemon configuration”GET /api/v1/system/daemon
Section titled “GET /api/v1/system/daemon”Returns the array of daemon programs loaded from the hoody-daemon configuration. Each entry includes the daemon’s name, the command used to launch it, and whether it is currently enabled.
This endpoint takes no parameters.
curl -X GET 'https://{projectId}-{containerId}-terminal-1.{server}.containers.hoody.com/api/v1/system/daemon'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-terminal-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.terminal.system.listDaemonPrograms();Response
Section titled “Response”[ { "name": "nginx", "command": "/usr/sbin/nginx", "enabled": true }]Power management
Section titled “Power management”POST /api/v1/system/reboot
Section titled “POST /api/v1/system/reboot”Initiates a system reboot. The kernel enforces the permission check, so the caller must have root or sudo privileges. The optional delay parameter schedules the reboot for a future time, in seconds.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
delay | query | integer | No | Delay in seconds before reboot, 0..86400 (default: 0 for immediate). shutdown(8) schedules in whole minutes, so the server rounds UP to the nearest minute and reports the actual scheduled value as effective_minutes in the response. |
curl -X POST 'https://{projectId}-{containerId}-terminal-1.{server}.containers.hoody.com/api/v1/system/reboot?delay=60'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-terminal-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.terminal.system.reboot({ delay: 60 });Response
Section titled “Response”{ "success": true, "message": "Reboot scheduled in 1 minute(s) (requested 60 seconds, rounded up)", "delay_seconds": 60, "effective_minutes": 1}{ "error": "'delay' exceeds maximum (86400 seconds)."}{ "error": "Permission denied - requires root or sudo privileges"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
ROOT_REQUIRED | Reboot requires root privileges | Run with sudo or as root user | Run with sudo or as root user |
{ "error": "method_not_allowed", "allow": "POST"}{ "error": "Failed to execute reboot command"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
REBOOT_FAILED | Failed to execute reboot | Check system logs | Check system logs |
POST /api/v1/system/shutdown
Section titled “POST /api/v1/system/shutdown”Initiates a system shutdown. The kernel enforces the permission check, so the caller must have root or sudo privileges. The optional delay parameter schedules the shutdown for a future time, in seconds.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
delay | query | integer | No | Delay in seconds before shutdown, 0..86400 (default: 0 for immediate). shutdown(8) schedules in whole minutes, so the server rounds UP to the nearest minute and reports the actual scheduled value as effective_minutes in the response. |
curl -X POST 'https://{projectId}-{containerId}-terminal-1.{server}.containers.hoody.com/api/v1/system/shutdown?delay=60'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-terminal-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.terminal.system.shutdown({ delay: 60 });Response
Section titled “Response”{ "success": true, "message": "Shutdown scheduled in 1 minute(s) (requested 60 seconds, rounded up)", "delay_seconds": 60, "effective_minutes": 1}{ "error": "'delay' exceeds maximum (86400 seconds)."}{ "error": "Permission denied - requires root or sudo privileges"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
ROOT_REQUIRED | Shutdown requires root privileges | Run with sudo or as root user | Run with sudo or as root user |
{ "error": "method_not_allowed", "allow": "POST"}{ "error": "Failed to execute shutdown command"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
SHUTDOWN_FAILED | Failed to execute shutdown | Check system logs | Check system logs |
Client diagnostics beacon
Section titled “Client diagnostics beacon”POST /api/v1/terminal/state
Section titled “POST /api/v1/terminal/state”Optional telemetry endpoint. The terminal client posts its own render and connection health (frontend build, effective renderer, the reason for the beacon) so the server can correlate client-side stalls with server-side session state.
This endpoint takes no parameters.
Request body
Section titled “Request body”| Field | Type | Required | Description |
|---|---|---|---|
build_id | string | No | Frontend build identifier |
renderer | string | No | Effective renderer (webgl or dom) |
reason | string | No | What triggered this beacon |
curl -X POST 'https://{projectId}-{containerId}-terminal-1.{server}.containers.hoody.com/api/v1/terminal/state' \ -H 'Content-Type: application/json' \ -d '{"build_id": "2026.04.14-rc1", "renderer": "webgl", "reason": "stall-detected"}'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-terminal-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.terminal.sessions.reportDiagnostics({ build_id: '2026.04.14-rc1', renderer: 'webgl', reason: 'stall-detected' });Response
Section titled “Response”{}The server acknowledges the beacon. No fields are returned.