The RunDock daemon exposes a full HTTP REST API on
http://127.0.0.1:2999/api/v1. All request and response bodies use JSON. All endpoints return standard HTTP status codes.
http://127.0.0.1:2999/api/v1
The host and port can be changed when starting the daemon (alter daemon start --port 3100).
By default the API is unauthenticated and binds to 127.0.0.1 (loopback only). Once a dashboard password is configured, all API routes require a valid bearer token.
Removing the dashboard password returns the daemon to passwordless mode. Keep the daemon bound to a loopback address when using this mode.
| Token | Source | Expiry |
|---|---|---|
| Session token | POST /auth/login or POST /auth/setup |
24 hours |
| Master token | %APPDATA%\alter-pm2\auth.json (CLI only) |
Never |
Authorization: Bearer <token>For EventSource / SSE connections that cannot set request headers, first use the bearer token to request a one-time, path-bound stream ticket. The ticket expires after 30 seconds and is consumed by the first matching request; never put a session or master token in a URL.
POST /api/v1/stream-ticket
Authorization: Bearer <session-token>
Content-Type: application/json
{ "path": "/processes/<id>/logs/stream" }Then open /api/v1/processes/<id>/logs/stream?ticket=<one-time-ticket> with EventSource.
Unauthenticated response (401):
{ "error": "Unauthorized" }Success:
{ "success": true, "message": "..." }Error:
{ "error": "process not found: my-app" }Process object (returned by most process endpoints):
{
"id": "3f2a1b4c-5d6e-7f8a-9b0c-1d2e3f4a5b6c",
"name": "api",
"script": "python",
"args": ["-m", "uvicorn", "main:app"],
"cwd": "C:\\projects\\api",
"status": "running",
"pid": 14820,
"restart_count": 0,
"uptime_secs": 7532,
"last_exit_code": null,
"autorestart": true,
"max_restarts": 10,
"watch": false,
"namespace": "web",
"cpu_percent": 1.4,
"memory_bytes": 52428800,
"env": { "PORT": "8000" },
"notify": null,
"created_at": "2026-02-22T09:00:00Z",
"started_at": "2026-02-22T09:00:00Z",
"stopped_at": null
}
cpu_percentandmemory_bytesarenullwhen the process is not running.notifyholds a per-process notification config override (see Notification Endpoints).
List all managed processes.
Response:
{
"processes": [ /* array of process objects */ ]
}Start a new process.
Request body:
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
script |
string | yes | — | Executable to run |
name |
string | no | derived from script | Display name |
args |
string[] | no | [] |
Arguments |
cwd |
string | no | null | Working directory |
env |
object | no | {} |
Environment variables |
autorestart |
bool | no | true |
Restart on crash |
max_restarts |
number | no | 10 |
Max restart attempts |
restart_delay_ms |
number | no | 1000 |
Base restart delay (ms) |
namespace |
string | no | "default" |
Process group |
watch |
bool | no | false |
Enable watch mode |
watch_paths |
string[] | no | [] |
Paths to watch |
watch_ignore |
string[] | no | [] |
Patterns to ignore |
max_log_size_mb |
number | no | 10 |
Log rotation threshold |
cron |
string | no | null | Cron expression for scheduled execution |
notify |
NotificationConfig | no | null | Process-level notification override |
log_alert |
LogAlertOverride | no | null | Process-level log alert override |
Example:
POST /api/v1/processes
{
"script": "python",
"name": "api",
"args": ["-m", "uvicorn", "main:app", "--port", "8000"],
"cwd": "C:\\projects\\api",
"env": { "PORT": "8000" },
"autorestart": true,
"namespace": "web"
}Response: 201 Created — the newly created process object.
Get a single process by UUID or name.
GET /api/v1/processes/api
GET /api/v1/processes/3f2a1b4c-5d6e-7f8a-9b0c-1d2e3f4a5b6c
Response: process object.
Update a process's configuration and apply immediately. The process is restarted with the new config.
Request body: same fields as POST /processes (all optional except script). Omitted fields preserve their current values.
Example:
PATCH /api/v1/processes/api
{
"script": "python",
"args": ["-m", "uvicorn", "main:app", "--port", "9000"],
"env": { "PORT": "9000" }
}Response: updated process object.
Stop and permanently remove a process from the registry.
DELETE /api/v1/processes/api
Response:
{ "success": true, "message": "process deleted" }Log files are NOT deleted. Use
DELETE /processes/{id}/logsto clear them.
Start a stopped process.
POST /api/v1/processes/api/start
Response: process object (status will be running or starting).
Stop a running process.
POST /api/v1/processes/api/stop
Response: process object (status will be stopped or stopping).
Stop and immediately restart a process.
POST /api/v1/processes/api/restart
Response: process object.
Reset the restart counter to zero.
POST /api/v1/processes/api/reset
Response: process object with restart_count: 0.
Open a terminal window in the process's working directory.
POST /api/v1/processes/api/terminal
Behavior:
- Windows: Tries Windows Terminal (
wt --startingDirectory <cwd>), falls back tostart cmd.exe - Linux/macOS: Opens
xtermin the working directory
Response:
{ "success": true, "message": "terminal opened" }Retrieve historical log lines from disk.
Query parameters:
| Parameter | Default | Description |
|---|---|---|
lines |
100 |
Number of lines to return |
type |
all |
Stream filter: all, stdout, or stderr |
date |
latest | Historical date in YYYY-MM-DD format |
Examples:
GET /api/v1/processes/api/logs
GET /api/v1/processes/api/logs?lines=500
GET /api/v1/processes/api/logs?type=stderr
GET /api/v1/processes/api/logs?date=2026-02-20&lines=200
Response:
{
"lines": [
{ "stream": "stdout", "content": "Server started on port 8000" },
{ "stream": "stderr", "content": "WARNING: debug mode enabled" }
]
}List available historical log dates for a process.
GET /api/v1/processes/api/logs/dates
Response:
{
"dates": ["2026-02-20", "2026-02-21", "2026-02-22"]
}Dates are returned in ascending order. Use a date from this list as the date query parameter in GET /processes/{id}/logs.
Stream log lines in real time using Server-Sent Events (SSE).
GET /api/v1/processes/api/logs/stream
Connection: Keep-alive, text/event-stream
Event data format:
{
"timestamp": "2026-02-22T10:30:00.123Z",
"stream": "stdout",
"content": "Handling GET /health"
}Keepalive: A comment event (: keepalive) is sent every 15 seconds to detect dead connections.
Client example (JavaScript):
const path = '/processes/api/logs/stream';
const ticketResponse = await fetch('/api/v1/stream-ticket', {
method: 'POST',
headers: {
Authorization: `Bearer ${sessionToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ path }),
});
if (!ticketResponse.ok) throw new Error(`ticket request failed: ${ticketResponse.status}`);
const { ticket } = await ticketResponse.json();
const es = new EventSource(`/api/v1${path}?ticket=${encodeURIComponent(ticket)}`);
es.onmessage = (e) => {
const line = JSON.parse(e.data);
console.log(`[${line.stream}] ${line.content}`);
};Check daemon status. Use this to detect if the daemon is running.
GET /api/v1/system/health
Response:
{
"status": "ok",
"version": "1.1.0",
"uptime_secs": 3600,
"process_count": 5,
"persistence_healthy": true,
"persistence_error": null
}Persist the current process list to disk.
POST /api/v1/system/save
Response:
{ "success": true, "message": "state saved" }Restore processes from the last saved state file.
POST /api/v1/system/resurrect
Response:
{ "success": true, "message": "restored 5 processes" }Gracefully shut down the daemon. Saves state before exiting.
POST /api/v1/system/shutdown
Response:
{ "success": true, "message": "daemon shutting down" }The daemon saves state and exits after a 200ms delay. The response is returned before shutdown completes.
Saves state, then restarts the daemon without dropping managed processes.
POST /api/v1/system/restart
Behaviour:
- Validates and saves the current process/project state
- Directly starts the same executable as a replacement with a private handoff token and readiness file
- Stops accepting mutations, joins background writers, and releases listener/PID ownership only after the final save
- Accepts the replacement only after its PID identity, port ownership, and health response match; otherwise terminates it and resumes the current daemon
- Managed processes survive and are re-adopted only when their persisted PID identity still matches
Response:
{ "success": true, "message": "daemon restarting" }All auth endpoints live under /api/v1/auth. The auth endpoints themselves are not protected by the middleware (so you can log in without a token). All other endpoints require a valid token when a password is configured.
Check whether authentication is configured.
GET /api/v1/auth/status
Response:
{
"password_configured": true,
"pin_configured": false,
"passkeys_count": 0,
"passkeys_supported": false,
"lock_timeout_mins": null
}First-time password setup. Returns 409 Conflict if a password is already set.
POST /api/v1/auth/setup
{ "password": "my-secure-password" }
Response:
{
"session_token": "<64-char hex token>",
"expires_at": "2026-03-12T11:00:00Z"
}Password-based login.
POST /api/v1/auth/login
{ "password": "my-secure-password" }
Response: same as /auth/setup (session_token, expires_at).
Errors: 401 Unauthorized — invalid password.
PIN-based quick login (4 or 6 digits).
POST /api/v1/auth/pin/login
{ "pin": "1234" }
Response: same as /auth/login.
Errors: 401 Unauthorized — invalid or unconfigured PIN.
Logout — invalidates the bearer token sent in the Authorization header.
DELETE /api/v1/auth/session
Authorization: Bearer <session_token>
Response:
{ "success": true }Change the dashboard password. Requires the current password.
POST /api/v1/auth/change-password
{
"current_password": "old-password",
"new_password": "new-secure-password"
}
Response:
{ "success": true }Disable dashboard authentication. When a password is configured, this request requires a valid session token or CLI master token. The operation removes the password, PIN, passkeys, auto-lock setting, and active browser sessions while preserving the CLI master token.
DELETE /api/v1/auth/password
Authorization: Bearer <session_or_master_token>
Response:
{ "success": true }After this succeeds, protected API routes accept requests without an Authorization header because no dashboard password is configured.
Set or update the quick-unlock PIN (4 or 6 digits only, numeric).
POST /api/v1/auth/pin
{ "pin": "1234" }
Response:
{ "success": true }Remove the configured PIN.
DELETE /api/v1/auth/pin
Response:
{ "success": true }Update authentication settings.
PATCH /api/v1/auth/settings
{ "lock_timeout_mins": 30 }
| Field | Type | Description |
|---|---|---|
lock_timeout_mins |
number | null | Auto-lock after this many minutes of inactivity. null disables auto-lock. |
Response:
{ "success": true }All Telegram endpoints live under /api/v1/telegram. The bot token is stored server-side and is never returned to the client in plaintext (only the last 4 characters are shown).
Return the current Telegram configuration.
GET /api/v1/telegram
Response:
{
"enabled": true,
"bot_token_set": true,
"bot_token_hint": "****xYzW",
"allowed_chat_ids": [123456789],
"notify_on_crash": true,
"notify_on_restart": true,
"notify_on_start": false,
"notify_on_stop": false
}Update the Telegram configuration. All fields are optional — omitted fields retain their current values.
PUT /api/v1/telegram
{
"enabled": true,
"bot_token": "123456:ABCDEFabcdef...",
"allowed_chat_ids": [123456789],
"notify_on_crash": true,
"notify_on_restart": true,
"notify_on_start": false,
"notify_on_stop": false
}
Send
"bot_token": ""to clear the stored token.
Response:
{ "success": true }Send a test message to a Telegram chat ID using the currently stored bot token.
POST /api/v1/telegram/test
{ "chat_id": 123456789 }
Response:
{ "success": true }Fetch the bot's Telegram username and display name by calling the Telegram API with the stored token.
GET /api/v1/telegram/botinfo
Response (success):
{ "ok": true, "username": "MyRunDockBot", "first_name": "RunDock" }Response (failure):
{ "ok": false, "username": null, "first_name": null, "error": "invalid token" }Load an ecosystem config file and start all apps defined in it.
POST /api/v1/ecosystem
{
"path": "C:\\projects\\alter.config.toml"
}
Response:
{ "success": true, "message": "loaded 3 apps" }Notification settings are stored at %APPDATA%\alter-pm2\notifications.json and survive daemon restarts.
NotificationConfig object:
{
"webhook": { "url": "https://example.com/hook", "enabled": true },
"slack": { "webhook_url": "https://hooks.slack.com/...", "enabled": true, "channel": "#alerts" },
"teams": { "webhook_url": "https://outlook.office.com/...", "enabled": false },
"discord": { "webhook_url": "https://discord.com/api/webhooks/...", "enabled": false },
"events_override": true,
"events": { "on_crash": true, "on_restart": true, "on_start": false, "on_stop": false }
}All channel fields are optional — omit any you don't need.
channelon Slack overrides the webhook's default channel. Setevents_override: truewhen this scope must explicitly override inherited event flags, including the valid “all events disabled” case; legacy records without it continue to inherit when every event flag is false.
Update only the process-level notification override without restarting the process.
PATCH /api/v1/processes/api/notifications
{ "notify": { /* NotificationConfig, or null to inherit */ } }Response: the updated process object.
Return the notification store shape (global config + all namespace overrides). Secret URL fields are redacted as __RUNDOCK_SECRET_SET__; sending that placeholder back in an update preserves the currently stored secret instead of replacing it.
GET /api/v1/notifications
Response:
{
"global": { /* NotificationConfig */ },
"namespaces": {
"web": { /* NotificationConfig */ }
}
}Update the global notification config. Applies to all processes not overridden at namespace or process level.
PUT /api/v1/notifications/global
{ /* NotificationConfig */ }
Response:
{ "success": true, "message": "global notifications updated" }Set a notification config override for a specific namespace. Takes priority over global for all processes in that namespace.
PUT /api/v1/notifications/namespace/web
{ /* NotificationConfig */ }
Response:
{ "success": true, "message": "namespace 'web' notifications updated" }Remove the namespace notification override (falls back to global config).
DELETE /api/v1/notifications/namespace/web
Response:
{ "success": true, "message": "namespace 'web' removed" }Fire a test notification using the provided config without affecting any real process. Useful for verifying webhook URLs and credentials.
POST /api/v1/notifications/test
{ /* NotificationConfig */ }
Response:
{ "success": true, "message": "test notification delivered to 1 target(s)" }Config cascade priority: process-level notify → namespace config → global config. The first non-null value per channel wins.
Projects are stable logical groups. A managed project aggregates one or more technical processes; a desktop project is a persistent software launcher with zero process members and status: "desktop". Older project records without kind remain managed.
| Method | Endpoint | Purpose |
|---|---|---|
GET |
/projects |
List project aggregates, members, status, CPU, and memory |
GET |
/projects/{id} |
Get one project |
PATCH |
/projects/{id} |
Update display metadata, managed state, or kind / launch_uri |
POST |
/projects/{id}/start |
Start all stopped enabled members |
POST |
/projects/{id}/stop |
Stop all active members |
POST |
/projects/{id}/restart |
Restart all enabled members |
PATCH |
/processes/{id}/project |
Assign a process with { "project_id": "uuid" } |
Project action responses include one result per component. success is false when any component fails, and the corresponding result contains an explicit error.
Desktop launch URIs must use a validated non-HTTP custom protocol such as wanmotai://open; http, https, file, javascript, and data are rejected. Desktop projects reject start, stop, restart, enable, and disable operations with HTTP 409 Conflict.
The detailed examples above cover the most common calls. This inventory is kept aligned with src/api/mod.rs and the Axum route declarations so less common capabilities remain discoverable. All routes use the /api/v1 prefix; only /auth/* is public. Streaming routes require a short-lived ticket from POST /stream-ticket.
| Area | Current routes |
|---|---|
| Process extensions | GET /processes/{id}/metrics/history, GET /processes/{id}/logs/stats, GET /processes/{id}/cron/history, PATCH /processes/{id}/enabled, PATCH /processes/{id}/project, PATCH /processes/{id}/notifications, POST /processes/{id}/clone, GET /processes/{id}/envfiles, GET/PUT /processes/{id}/envfile |
| Namespace lifecycle | POST /processes/namespace/{ns}/start, /stop, /restart |
| Git | GET /processes/{id}/git, POST /processes/{id}/git/pull |
| System | GET /system/stats, /paths, /check-env, /list-env, /read-env, /browse; POST /system/write-env, /sync-env, /open-folder; GET/PUT /system/ui-settings; PUT /system/ui-settings/view-mode |
| Saved scripts | GET/POST /scripts, GET/DELETE /scripts/{name}, GET /scripts/{name}/run |
| AI | GET/PUT /ai/settings, POST /ai/chat, POST /ai/auth/start, GET /ai/auth/status, DELETE /ai/auth, GET /ai/models |
| Ports | GET /ports, POST /ports/kill/{pid} (body must repeat the confirmed port and process name) |
| Tunnels | GET/POST /tunnels, POST /tunnels/{id}/stop, DELETE /tunnels/{id}, GET/PUT /tunnels/settings, POST /tunnels/settings/test, POST /tunnels/settings/install, GET /tunnels/settings/install/stream |
| Terminals | GET /terminals, GET /terminals/ws, GET/PUT /terminals/history/{key} |
| Operations | GET /metrics, GET/PUT /log-alerts, PUT/DELETE /log-alerts/namespace/{ns}, GET /system/update/check, POST /system/update/apply |
| Code | Meaning |
|---|---|
200 OK |
Successful GET, POST (non-create) |
201 Created |
Process created (POST /processes) |
400 Bad Request |
Invalid request body or parameters |
401 Unauthorized |
Missing or invalid bearer token |
404 Not Found |
Process not found |
409 Conflict |
Resource already exists (e.g. password already set) |
500 Internal Server Error |
Unexpected server error |
Same-origin calls are supported normally. Cross-origin browser calls are accepted only from loopback HTTP origins on the daemon port or the Vite development port 5173; arbitrary internet, LAN, null, file, and HTTPS origins are rejected. Allowed methods are GET, POST, PUT, PATCH, DELETE, and OPTIONS, with Authorization and Content-Type request headers.
PowerShell:
# Start a process
Invoke-RestMethod -Uri "http://localhost:2999/api/v1/processes" `
-Method POST -ContentType "application/json" `
-Body '{"script":"python","name":"api","args":["-m","http.server","8080"]}'
# List processes
Invoke-RestMethod -Uri "http://localhost:2999/api/v1/processes"
# Stop a process
Invoke-RestMethod -Uri "http://localhost:2999/api/v1/processes/api/stop" -Method POST
# Health check
Invoke-RestMethod -Uri "http://localhost:2999/api/v1/system/health"curl:
# List processes
curl http://localhost:2999/api/v1/processes
# Start a process
curl -X POST http://localhost:2999/api/v1/processes \
-H "Content-Type: application/json" \
-d '{"script":"python","name":"api","args":["-m","http.server","8080"]}'
# Stream logs after requesting a one-time ticket with an authenticated bearer request
ticket=$(curl -fsS -X POST http://localhost:2999/api/v1/stream-ticket \
-H "Authorization: Bearer $ALTER_SESSION_TOKEN" \
-H "Content-Type: application/json" \
-d '{"path":"/processes/api/logs/stream"}' | jq -r .ticket)
curl -N "http://localhost:2999/api/v1/processes/api/logs/stream?ticket=$ticket"