Program Control
Section titled “Program Control”Use these endpoints to enable, disable, start, and stop daemon programs managed by supervisord. Enable and disable change a program’s registration state. A disable that answers 200 has removed the program from supervisord’s configuration, so nothing can start it; a disable whose configuration removal failed answers 400 with the program already persisted as disabled and its configuration still loaded, and it has to be disabled again once the cause is fixed. Start requires an enabled program; stop works whatever the enabled flag says.
Enable and disable
Section titled “Enable and disable”Enable and disable set whether a program may run. Disabling a running program removes its supervisord definition and stops it as part of the same call; enabling writes the definition only for programs the daemon applies, as described below.
POST /api/v1/daemon/programs/{id}/enable
Section titled “POST /api/v1/daemon/programs/{id}/enable”Sets enabled: true and saves it. The supervisord definition is written at once only for a program with boot, lazy_load, a port_range or an effective sandbox, so enabling a boot: true program can start it; an ordinary program is registered when it is first started. A sandboxed program whose definition cannot be applied is a 400 after the flag was saved (enabled but its supervisord config was NOT applied); an ordinary program’s apply failure still returns 200, so a 200 does not prove the definition was installed.
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) never reaches the handler: the path extractor refuses it with a plain-text 404, on every route that takes an id, which is NOT the documented JSON 404 of the routes that have one. An id that parses but names no program is the handler’s own answer, a 400 carrying the {success: false, error: ...} envelope, except on the routes that document a 404. |
Response
Section titled “Response”{ "success": true, "program": { "id": 1, "name": "web-server", "description": "Nginx web server", "enabled": true, "command": "nginx -g \"daemon off;\"", "boot": true, "delay_seconds": 5, "autorestart": "unexpected", "user": "www-data", "environment": {}, "directory": "/var/www", "priority": 999 }}An unknown id is a 400, not a 404. Persistence and internal failures answer 500 instead, as described below.
{ "success": false, "error": "Program with ID 999 not found"}The program could not be persisted, or a request carrying an effective sandbox block could not read or apply the sandbox firewall state. The error string starts with PERSIST: for the first case and INTERNAL: for the second. An unsandboxed request never fails on the firewall state.
{ "success": false, "error": "PERSIST: failed to save programs.json: No space left on device"}Code example
Section titled “Code example”# Enable a programhoody daemon programs enable "$PROGRAM_ID" -c "$CONTAINER_ID"curl -X POST "https://{projectId}-{containerId}-daemon-1.{server}.containers.hoody.com/api/v1/daemon/programs/100/enable" \ -H "Authorization: Bearer <token>"import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-daemon-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.daemon.programs.enable(100);POST /api/v1/daemon/programs/{id}/disable
Section titled “POST /api/v1/daemon/programs/{id}/disable”Disables the program and removes it from supervisord configuration. The program will be stopped if currently running. The row is persisted first and the supervisord configuration is removed afterwards, so a removal that fails answers 400 saying the program is disabled but its supervisord configuration was not removed. Repair the cause and disable the program again: no later startup removes that configuration, because the startup sweep only collects configurations whose program id is gone. When the daemon kept firewall rules for a definition that may still be running unconfined, the same message says so.
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) never reaches the handler: the path extractor refuses it with a plain-text 404, on every route that takes an id, which is NOT the documented JSON 404 of the routes that have one. An id that parses but names no program is the handler’s own answer, a 400 carrying the {success: false, error: ...} envelope, except on the routes that document a 404. |
Response
Section titled “Response”{ "success": true, "program": { "id": 1, "name": "web-server", "description": "Nginx web server", "enabled": false, "command": "nginx -g \"daemon off;\"", "boot": true, "delay_seconds": 5, "autorestart": "unexpected", "user": "www-data", "environment": {}, "directory": "/var/www", "priority": 999 }}An unknown id is a 400, not a 404. Persistence and internal failures answer 500 instead, as described below.
{ "success": false, "error": "Program with ID 999 not found"}The program could not be persisted: the programs.json write failed and the error string starts with PERSIST: . This endpoint makes no firewall admission, so there is nothing to revert and no INTERNAL: failure to report. A firewall-record retirement that fails does NOT produce a 500 either: the change completes, the failure is logged, the record stays with its rules and its port reservation, and the next daemon start reconciles it.
{ "success": false, "error": "PERSIST: failed to save programs.json: No space left on device"}Code example
Section titled “Code example”# Disable a program, which stops ithoody daemon programs disable "$PROGRAM_ID" -c "$CONTAINER_ID"curl -X POST "https://{projectId}-{containerId}-daemon-1.{server}.containers.hoody.com/api/v1/daemon/programs/100/disable" \ -H "Authorization: Bearer <token>"import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-daemon-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.daemon.programs.disable(100);Start and stop
Section titled “Start and stop”Start (on an enabled program) and stop act immediately via supervisorctl. Both need a JSON body with Content-Type: application/json, at least {}: a missing body, a request without the right Content-Type, a body that is not JSON, or a field of the wrong type is a 400 carrying the {success: false, error} envelope, and a body over 1 MiB a 413. Port-range programs are multi-instance, with each port in the configured range as a separate running process, and these endpoints address one instance at a time unless all: true is sent to stop.
POST /api/v1/daemon/programs/{id}/start
Section titled “POST /api/v1/daemon/programs/{id}/start”Issues the start immediately via supervisorctl. For port-range programs, the port parameter is required to specify which instance to start. The optional wait parameter blocks until the program reaches the RUNNING state, and if_not_running makes the call idempotent. The program must be enabled before it can be started.
Without wait, a 200 reports that the start was issued, not that the program is running: a newly started port-range instance gets instance.status: "starting", and a standard program returns only success (plus already_running in idempotent mode). Send wait: true, or poll GET /status/{id}, to learn the outcome. A confirmed start failure, such as a start supervisord refused, is a 400 whose error says why (program <name> did not start: <reason>). When it was the first attempt that failed, supervisord may still retry the program and it may come up, so check its status.
A standard program answers {"success": true} (plus already_running with if_not_running); only a port-range start carries instance.
For a sandboxed program, a launch the sandbox refuses for its configuration exits 78 with a sandbox: <reason> line on stderr, a launch cancelled by a stop signal or a failed handshake exits 143 unless a wrapper stage already exited with its own nonzero status, and a signal death is 128+n. Status shows only the state, typically fatal, so read the program’s stderr log.
With wait: true, a program that finishes during the wait answers 200 with status: "exited" and its last_run, whatever the exit code; a start that ends in fatal, or that supervisord refused, is a 400 that carries last_run. A finished run never counts as ready: a caller that needs the program to serve (a ready_port, or if_not_running) gets a 400 saying it exited before accepting connections. A start whose reply from supervisord was lost answers 502 and is never retried: read the program’s status before starting it again.
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) never reaches the handler: the path extractor refuses it with a plain-text 404, on every route that takes an id, which is NOT the documented JSON 404 of the routes that have one. An id that parses but names no program is the handler’s own answer, a 400 carrying the {success: false, error: ...} envelope, except on the routes that document a 404. |
Request Body
Section titled “Request Body”| Field | Type | Required | Default | Description |
|---|---|---|---|---|
port | integer | No (required for port-range programs) | — | Port number to start (required for port-range programs) |
wait | boolean | No | false | Poll until the program is running and, when it declares ready_port, until 127.0.0.1:<ready_port> accepts a TCP connection. A listener bound only to another address never satisfies it. |
timeout | integer | No | 30 | Readiness polling budget in seconds (0 to 4294967295). Configuration and the supervisord start happen before it begins, so the request can take longer; it is not an HTTP deadline. |
if_not_running | boolean | No | false | Idempotent mode: reports already_running. Concurrent idempotent starts for the same id and port share one start whatever their own wait and timeout, so a waiting caller can receive an earlier non-waiting caller’s starting. After it finished, only a waited result that found the program ready is reused, for about 2 seconds, and only while that run is observably alive and has not reported an end; anything else (a start nobody waited for, a failed or finished result, a run the daemon cannot observe) is not reused, and the next caller checks again and starts the program if it is not running. A caller whose wait for a shared start runs out gets a 400 (start is still in progress ... retry) and no second start. The already-running shortcut for a port-range instance does not check ready_port. |
{ "port": 8042}{ "port": 8042, "wait": true, "timeout": 60}{}{ "wait": true, "timeout": 30}{ "port": 8042, "if_not_running": true}{ "port": 8042, "if_not_running": true, "wait": true, "timeout": 60}Response
Section titled “Response”{ "success": true}{ "success": true, "instance": { "port": 8042, "instance_name": "api-server_8042", "status": "starting" }}{ "success": true, "already_running": true, "instance": { "port": 8042, "instance_name": "api-server_8042", "status": "running", "pid": 12345, "uptime": "0:15:30" }}{ "success": true, "already_running": false, "instance": { "port": 8042, "instance_name": "api-server_8042", "status": "starting" }}With wait: true, a one-shot command that finished during the wait. status is present only when a waiting start saw the program finish (exited or stopped), and last_run carries its result. A port-range start carries last_run inside instance instead.
{ "success": true, "status": "exited", "last_run": { "exit_code": 0, "signal": null, "error": null, "ended_at": 1791370004210, "final": true }}{ "success": false, "error": "Port parameter required for port-range programs. Available range: 8000-8099"}An unknown id is a 400, not a 404. The same status covers a start supervisord refused or gave up on (fatal), with last_run when a result is known, and a start whose caller needs the program to serve (a ready_port, or if_not_running) when the program finished instead: program exited before accepting connections (exit code N). Persistence and internal failures answer 500 instead, as described below.
{ "success": false, "error": "Program with ID 999 not found"}The request body is larger than the daemon’s whole-body JSON limit of 1 MiB. Refused before the handler runs, with the {success: false, error} envelope. Separate from the sandbox policy’s own 64 KiB rendered-spec ceiling, which is a 400 from the validator.
{ "success": false, "error": "the request body is larger than the 1 MiB limit"}A start that carries an effective sandbox block could not read or apply the sandbox firewall state, and the error string starts with INTERNAL: . Start persists nothing, so it has no PERSIST: failure, and an unsandboxed start never fails on the firewall state.
{ "success": false, "error": "INTERNAL: sandbox firewall state could not be read or applied"}The start reached supervisord and its reply was lost, or a run that had ended could not be confirmed stopped before the new start: the error starts with INDETERMINATE: . The program may be running. Nothing is retried; read its status before starting again.
{ "success": false, "error": "INDETERMINATE: program web-server start: the start of web-server reached supervisord and its reply was lost (read: Connection reset by peer (os error 104)); it may have started"}Code example
Section titled “Code example”# Start one instance. The CLI's own HTTP timeout is 30 seconds, so do not rely on# --wait for longer readiness budgets: start, then poll the instance's status.hoody daemon programs start "$PROGRAM_ID" --port 8042 -c "$CONTAINER_ID"hoody -o json daemon programs status "$PROGRAM_ID" --port 8042 -c "$CONTAINER_ID"curl -X POST "https://{projectId}-{containerId}-daemon-1.{server}.containers.hoody.com/api/v1/daemon/programs/100/start" \ -H "Authorization: Bearer <token>" \ -H "Content-Type: application/json" \ -d '{"port": 8042, "wait": true, "timeout": 60}'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-daemon-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.daemon.programs.start(100, { port: 8042, wait: true, timeout: 60 });POST /api/v1/daemon/programs/{id}/stop
Section titled “POST /api/v1/daemon/programs/{id}/stop”Issues the stop immediately via supervisorctl. For port-range programs, specify port to stop a specific instance or all: true to stop every instance; when both are sent, port wins. A standard program ignores both fields, and a stop does not require the program to be enabled. The handler discards the result of the underlying supervisorctl stop, so a 200 reports that the stop was issued rather than that the process is gone; read GET /status/{id} to confirm.
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) never reaches the handler: the path extractor refuses it with a plain-text 404, on every route that takes an id, which is NOT the documented JSON 404 of the routes that have one. An id that parses but names no program is the handler’s own answer, a 400 carrying the {success: false, error: ...} envelope, except on the routes that document a 404. |
Request Body
Section titled “Request Body”| Field | Type | Required | Default | Description |
|---|---|---|---|---|
port | integer | No | — | Specific port to stop |
all | boolean | No | — | Stop all instances (for port-range programs) |
{ "port": 8042}{ "all": true}{}Response
Section titled “Response”{ "success": true}{ "success": false, "error": "Port parameter or \"all: true\" required to stop port-range program instances"}An unknown id is a 400, not a 404.
{ "success": false, "error": "Program with ID 999 not found"}The request body is larger than the daemon’s whole-body JSON limit of 1 MiB. Refused before the handler runs, with the {success: false, error} envelope. Separate from the sandbox policy’s own 64 KiB rendered-spec ceiling, which is a 400 from the validator.
{ "success": false, "error": "the request body is larger than the 1 MiB limit"}Code example
Section titled “Code example”# Stop every instance of a port-range programhoody daemon programs stop "$PROGRAM_ID" --all -c "$CONTAINER_ID"curl -X POST "https://{projectId}-{containerId}-daemon-1.{server}.containers.hoody.com/api/v1/daemon/programs/100/stop" \ -H "Authorization: Bearer <token>" \ -H "Content-Type: application/json" \ -d '{"all": true}'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-daemon-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.daemon.programs.stop(100, { all: true });