The alter 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, append ?token=<token> to the URL.
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 |
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 es = new EventSource('/api/v1/processes/api/logs/stream');
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": "0.3.0",
"uptime_secs": 3600,
"process_count": 5
}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:
- Saves the current process state to disk
- Removes the PID file
- Spawns a detached watcher (
cmd /C timeout 1 && alter daemon starton Windows,sh -c 'sleep 1 && alter daemon start'on Linux/macOS) - Exits — managed processes survive because they are spawned outside the daemon's job object
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 },
"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.
Return the full notifications store (global config + all namespace overrides).
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 dispatched" }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.
| 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 |
The API allows cross-origin requests from any origin. All HTTP methods and headers are permitted.
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
curl -N http://localhost:2999/api/v1/processes/api/logs/stream