openapi: 3.1.0 info: title: Sencho API version: 0.25.3 description: | REST API for Sencho, a self-hosted Docker Compose management dashboard. Sencho exposes a public API for automating stack deployments, managing webhooks, monitoring fleet health, and integrating with CI/CD pipelines. ## Authentication All authenticated endpoints accept a Bearer token in the `Authorization` header. Generate API tokens from **Settings > API Tokens** in the Sencho dashboard (admin role required). ``` Authorization: Bearer YOUR_API_TOKEN ``` ## Node Routing Sencho manages multiple nodes (local and remote). To target a specific node, include the `x-node-id` header or `?nodeId=` query parameter. If omitted, the request targets the default node. ## License Tiers Some endpoints require an Admiral license. Requests to gated endpoints on Community return `403` with `code: "PAID_REQUIRED"`. contact: name: Sencho url: https://sencho.io license: name: BSL 1.1 url: https://github.com/studio-saelix/sencho/blob/main/LICENSE servers: - url: "{protocol}://{host}:{port}" description: Your Sencho instance variables: protocol: default: https enum: [http, https] host: default: localhost port: default: "1852" security: - bearerAuth: [] tags: - name: Health description: Instance health check - name: Stacks description: Create, read, update, delete, and operate Docker Compose stacks - name: Containers description: List and manage running containers - name: API Tokens description: Manage scoped API tokens (admin role required) - name: Webhooks description: Configure and trigger deployment webhooks - name: Nodes description: Manage local and remote Sencho nodes - name: Fleet description: Multi-node fleet overview and snapshots - name: Scheduled Tasks description: Configure recurring automated operations (admin role required) - name: Registries description: Manage private container registry credentials (Admiral license required). These endpoints are only accessible via browser sessions — API tokens receive `SCOPE_DENIED`. - name: Image Updates description: Check for available container image updates across stacks components: securitySchemes: bearerAuth: type: http scheme: bearer bearerFormat: JWT description: API token generated from Settings > API Tokens. Scopes — `read-only`, `deploy-only`, or `full-admin`. webhookSignature: type: apiKey in: header name: X-Webhook-Signature description: HMAC-SHA256 signature for webhook trigger requests. Compute as `sha256=HMAC(request_body, webhook_secret)`. parameters: stackName: name: stackName in: path required: true description: > Stack directory name. Must match `^[a-zA-Z0-9_-]+$` (alphanumeric characters, hyphens, and underscores only). Returns `400 Invalid stack name` if the name contains path separators, dots, spaces, or other special characters. schema: type: string pattern: '^[a-zA-Z0-9_-]+$' example: my-stack nodeId: name: x-node-id in: header required: false description: Target a specific node by ID. If omitted, the default node is used. Can also be passed as `?nodeId=` query parameter. schema: type: integer example: 1 nodeIdQuery: name: nodeId in: query required: false description: Target a specific node by ID (alternative to `x-node-id` header). schema: type: integer example: 1 idPath: name: id in: path required: true description: Numeric resource ID. schema: type: integer schemas: Error: type: object required: [error] properties: error: type: string description: Human-readable error message. code: type: string description: Machine-readable error code (e.g., `PAID_REQUIRED`, `SCOPE_DENIED`). enum: [PAID_REQUIRED, SCOPE_DENIED] SuccessMessage: type: object required: [message] properties: message: type: string SuccessBoolean: type: object required: [success] properties: success: type: boolean example: true LabelSource: type: string description: Provenance of a label. `unknown` when a container or image could not be inspected. enum: [compose, runtime, image, compose-system, unknown] LabelValue: type: object required: [key, value, source] properties: key: { type: string } value: type: string description: Redacted to `[redacted]` for secret-like keys unless the caller is an admin and passes `reveal=1`. source: { $ref: "#/components/schemas/LabelSource" } redacted: { type: boolean } LabelIndexContainerRef: type: object required: [id, name, stack, service] properties: id: { type: string } name: { type: string } stack: { type: string, nullable: true } service: { type: string, nullable: true } nodeId: { type: integer } nodeName: { type: string } LabelIndexRow: type: object description: One unique key/value/source and every container carrying it. required: [key, value, source, containers] properties: key: { type: string } value: { type: string } source: { $ref: "#/components/schemas/LabelSource" } redacted: { type: boolean } containers: type: array items: { $ref: "#/components/schemas/LabelIndexContainerRef" } ContainerLabelRow: type: object required: [id, name, stack, service, state, labels] properties: id: { type: string } name: { type: string } stack: { type: string, nullable: true } service: { type: string, nullable: true } state: { type: string } labels: type: array items: { $ref: "#/components/schemas/LabelValue" } StackLabelReplica: type: object required: [id, name, state, runtimeLabels, onlyInCompose, onlyOnContainer, inBoth, changed] properties: id: { type: string } name: { type: string } state: { type: string } runtimeLabels: type: array items: { $ref: "#/components/schemas/LabelValue" } onlyInCompose: { type: array, items: { type: string } } onlyOnContainer: { type: array, items: { type: string } } inBoth: { type: array, items: { type: string } } changed: type: array items: { type: string } description: Keys declared in Compose and present at runtime but with a different value. inspectFailed: type: boolean description: Runtime labels could not be read for this replica; reconciliation was skipped. StackServiceLabelRow: type: object required: [service, declaredLabels, replicas] properties: service: { type: string } declaredLabels: type: array items: { $ref: "#/components/schemas/LabelValue" } replicas: type: array items: { $ref: "#/components/schemas/StackLabelReplica" } StackLabelInventory: type: object required: [stackName, renderable, services, partial, generatedAt] properties: stackName: { type: string } renderable: type: boolean description: False when the Compose model could not be rendered; declared provenance is then unknown. partial: type: boolean description: A replica or its image could not be fully inspected. generatedAt: { type: integer } services: type: array items: { $ref: "#/components/schemas/StackServiceLabelRow" } NodeLabelInventory: type: object required: [nodeId, containers, byLabel, partial, generatedAt] properties: nodeId: { type: integer } partial: { type: boolean } generatedAt: { type: integer } containers: type: array items: { $ref: "#/components/schemas/ContainerLabelRow" } byLabel: type: array items: { $ref: "#/components/schemas/LabelIndexRow" } FleetLabelInventory: type: object required: [nodes, aggregatedByLabel, nodeErrors, generatedAt] properties: generatedAt: { type: integer } nodes: type: array items: type: object required: [nodeId, nodeName, status, inventory, error] properties: nodeId: { type: integer } nodeName: { type: string } status: { type: string, enum: [ok, error] } inventory: nullable: true allOf: [{ $ref: "#/components/schemas/NodeLabelInventory" }] error: { type: string, nullable: true } aggregatedByLabel: type: array items: { $ref: "#/components/schemas/LabelIndexRow" } nodeErrors: type: object additionalProperties: { type: string } description: Map of node id to error for nodes that were unreachable or returned a malformed payload. FailureClassification: type: object description: Classified cause of a failed deploy or update, with a suggested next step. required: [reason, label, suggestion] properties: reason: type: string enum: - image_pull_failed - compose_render_failed - env_missing - port_conflict - bind_path_missing - permission_denied - container_exited - healthcheck_failed - dependency_unavailable - node_unreachable - unknown label: type: string description: Short display headline for the cause. suggestion: type: string description: Suggested next action, one sentence. Container: type: object properties: Id: type: string description: Docker container ID. Names: type: array items: type: string description: Container names (prefixed with `/`). Service: type: string description: Compose service name (if part of a stack). State: type: string description: Container state. enum: [running, exited, paused, restarting, dead, created] Status: type: string description: Human-readable status string (e.g., "Up 2 hours"). Ports: type: array items: type: object properties: PrivatePort: type: integer PublicPort: type: integer Node: type: object required: [id, name, type, compose_dir, is_default, created_at] properties: id: type: integer name: type: string type: type: string enum: [local, remote] compose_dir: type: string description: Base directory for Docker Compose stacks on this node. is_default: type: integer enum: [0, 1] description: Whether this is the default node (1 = true). api_url: type: string description: Remote node API URL (only for remote nodes). api_token: type: string description: Redacted API token for remote node authentication. created_at: type: integer description: Unix timestamp (seconds). Webhook: type: object required: [id, name, stack_name, action, secret, enabled, created_at] properties: id: type: integer node_id: type: integer description: Node the webhook executes against. name: type: string stack_name: type: string action: type: string enum: [deploy, restart, stop, start, pull, git-pull] secret: type: string description: Masked secret (e.g., `****...****`). Full secret is only returned on creation. enabled: type: integer enum: [0, 1] created_at: type: integer description: Unix timestamp (seconds). WebhookExecution: type: object properties: id: type: integer webhook_id: type: integer triggered_at: type: integer description: Unix timestamp (seconds). status: type: string response: type: string error: type: ["string", "null"] ApiToken: type: object required: [id, name, scope, user_id, created_at] properties: id: type: integer name: type: string scope: type: string enum: [read-only, deploy-only, full-admin] user_id: type: integer created_at: type: integer description: Unix timestamp (seconds). expires_at: type: ["integer", "null"] description: Unix timestamp (seconds) or null for no expiry. revoked_at: type: ["integer", "null"] last_used_at: type: ["integer", "null"] FleetNodeOverview: type: object properties: id: type: integer name: type: string type: type: string enum: [local, remote] status: type: string enum: [online, offline] stats: type: ["object", "null"] description: Container statistics for the node. systemStats: type: ["object", "null"] description: Host system resource statistics. stacks: type: ["integer", "null"] description: Number of stacks on this node. FleetSnapshot: type: object required: [id, description, created_by, created_at, node_count, stack_count] properties: id: type: integer description: type: string created_by: type: string created_at: type: integer description: Unix timestamp (seconds). node_count: type: integer stack_count: type: integer skipped_nodes: type: string description: JSON-stringified array of skipped node names. FleetSnapshotDetail: allOf: - $ref: "#/components/schemas/FleetSnapshot" - type: object properties: nodes: type: array items: type: object properties: nodeId: type: integer nodeName: type: string stacks: type: array items: type: object properties: stackName: type: string files: type: array items: type: object properties: filename: type: string content: type: string ScheduledTask: type: object required: [id, name, target_type, action, cron_expression, enabled, created_by, created_at, updated_at] properties: id: type: integer name: type: string target_type: type: string enum: [stack, fleet, system, container] target_id: type: ["string", "null"] description: Stack or container name (when target_type is `stack` or `container`). node_id: type: ["integer", "null"] description: Target node ID (when target_type is `stack` or `container`). action: type: string enum: [restart, snapshot, prune] cron_expression: type: string description: Standard cron expression (5 fields). example: "0 3 * * *" enabled: type: integer enum: [0, 1] created_by: type: string created_at: type: integer description: Unix timestamp (seconds). updated_at: type: integer description: Unix timestamp (seconds). last_run_at: type: ["integer", "null"] next_run_at: type: ["integer", "null"] last_status: type: ["string", "null"] last_error: type: ["string", "null"] prune_targets: type: ["string", "null"] description: JSON-stringified array of prune targets (containers, images, networks, volumes). target_services: type: ["string", "null"] description: JSON-stringified array of service names to restart. prune_label_filter: type: ["string", "null"] description: Docker label filter for prune operations. ScheduledTaskRun: type: object required: [id, task_id, started_at, status, triggered_by] properties: id: type: integer task_id: type: integer started_at: type: integer description: Unix timestamp (seconds). completed_at: type: ["integer", "null"] triggered_by: type: string enum: [scheduled, manual] status: type: string enum: [pending, running, success, failed] output: type: ["string", "null"] error: type: ["string", "null"] Registry: type: object properties: id: type: integer name: type: string example: GitHub Container Registry url: type: string example: ghcr.io type: type: string enum: [dockerhub, ghcr, ecr, custom] username: type: string aws_region: type: ["string", "null"] description: AWS region (only present for ECR registries). created_at: type: integer description: Unix timestamp (seconds). RegistryCreate: type: object required: [name, url, type, username, secret] properties: name: type: string maxLength: 100 example: GitHub Container Registry url: type: string maxLength: 500 example: ghcr.io type: type: string enum: [dockerhub, ghcr, ecr, custom] username: type: string example: my-user secret: type: string description: Password, token, or access key. Write-only — never returned in GET responses. aws_region: type: string description: Required when `type` is `ecr`. example: us-east-1 ImageUpdateStatus: type: object properties: stack_name: type: string description: Name of the stack. has_updates: type: integer enum: [0, 1] description: Whether any images in this stack have newer versions available. last_checked: type: ["integer", "null"] description: Unix timestamp of the last check. ImageUpdateCheckStatus: type: object properties: checking: type: boolean description: "`true` if a scan is currently running." intervalMinutes: type: integer description: Configured fallback interval in minutes (15-1440). lastCheckedAt: type: ["integer", "null"] description: Unix epoch-ms of the last check start, or null if never checked. nextCheckAt: type: ["integer", "null"] description: Unix epoch-ms of the next scheduled check, or null when not scheduled. manualCooldownMinutes: type: integer description: Fixed cooldown ceiling in minutes for manual refresh. manualCooldownRemainingMs: type: integer description: Live remaining cooldown in milliseconds (0 when refresh is allowed). mode: type: string enum: [interval, cron] description: Active scheduling mode. cronExpression: type: ["string", "null"] description: 5-field cron expression when mode is 'cron', null otherwise. sidebarIndicators: type: boolean description: Whether sidebar update-status indicators are enabled. Controlled by the `image_update_sidebar_indicators` global setting. Default is `false`. responses: Unauthorized: description: Authentication required. Provide a valid Bearer token. content: application/json: schema: $ref: "#/components/schemas/Error" example: error: "Authentication required" Forbidden: description: Insufficient permissions or license tier. content: application/json: schema: $ref: "#/components/schemas/Error" example: error: "This feature requires a Sencho Admiral license." code: "PAID_REQUIRED" NotFound: description: Resource not found. content: application/json: schema: $ref: "#/components/schemas/Error" InternalError: description: Internal server error. content: application/json: schema: $ref: "#/components/schemas/Error" paths: # ── Health ────────────────────────────────────────────── /api/health: get: operationId: getHealth tags: [Health] summary: Health check description: Returns instance health status and uptime. No authentication required. Useful for uptime monitors and Docker HEALTHCHECK. security: [] responses: "200": description: Instance is healthy. content: application/json: schema: type: object required: [status, uptime] properties: status: type: string example: ok uptime: type: number description: Process uptime in seconds. /api/meta: get: operationId: getMeta tags: [Health] summary: Instance metadata description: Returns this Sencho instance's version and list of supported capabilities. No authentication required. Used by nodes to negotiate feature compatibility. security: [] responses: "200": description: Instance metadata. content: application/json: schema: type: object required: [version, capabilities] properties: version: type: string description: Sencho version (semver). example: "0.32.0" capabilities: type: array description: List of feature capabilities this instance supports. items: type: string example: ["stacks", "containers", "fleet", "auto-updates", "host-console"] /api/system/update: post: operationId: triggerSelfUpdate tags: [Health] summary: Trigger self-update description: | Instructs this Sencho instance to pull the latest Docker image and recreate its own container. Returns 202 immediately; the actual update happens asynchronously after the response is sent. Requires the instance to be deployed via Docker Compose with the Docker socket mounted. responses: "202": description: Update initiated. content: application/json: schema: type: object properties: message: type: string example: "Update initiated. The server will restart shortly." "401": $ref: "#/components/responses/Unauthorized" "503": description: Self-update not available (not running in Docker Compose). content: application/json: schema: $ref: "#/components/schemas/Error" # ── Stacks ────────────────────────────────────────────── /api/stacks: get: operationId: listStacks tags: [Stacks] summary: List all stacks description: Returns an array of stack directory names on the target node. parameters: - $ref: "#/components/parameters/nodeId" - $ref: "#/components/parameters/nodeIdQuery" responses: "200": description: Array of stack names. content: application/json: schema: type: array items: type: string example: ["traefik", "portainer", "monitoring"] "401": $ref: "#/components/responses/Unauthorized" "500": $ref: "#/components/responses/InternalError" post: operationId: createStack tags: [Stacks] summary: Create a new stack description: Creates a new stack directory with a default `compose.yaml` file. parameters: - $ref: "#/components/parameters/nodeId" requestBody: required: true content: application/json: schema: type: object required: [stackName] properties: stackName: type: string description: Stack name. Alphanumeric characters, hyphens, and underscores only. pattern: "^[a-zA-Z0-9_-]+$" example: my-new-stack responses: "201": description: Stack created. content: application/json: schema: type: object required: [message, name] properties: message: type: string example: Stack created successfully name: type: string example: my-new-stack "400": description: Invalid stack name. content: application/json: schema: $ref: "#/components/schemas/Error" "409": description: Stack already exists. content: application/json: schema: $ref: "#/components/schemas/Error" example: error: Stack already exists "500": $ref: "#/components/responses/InternalError" /api/stacks/{stackName}: get: operationId: getStack tags: [Stacks] summary: Get stack compose file description: Returns the raw `compose.yaml` content for the specified stack. parameters: - $ref: "#/components/parameters/stackName" - $ref: "#/components/parameters/nodeId" responses: "200": description: Raw compose.yaml content. content: text/plain: schema: type: string "400": description: Invalid stack name. content: application/json: schema: $ref: "#/components/schemas/Error" "500": $ref: "#/components/responses/InternalError" put: operationId: updateStack tags: [Stacks] summary: Update stack compose file description: Overwrites the `compose.yaml` content for the specified stack. Requires `stack:edit` permission. parameters: - $ref: "#/components/parameters/stackName" - $ref: "#/components/parameters/nodeId" requestBody: required: true content: application/json: schema: type: object required: [content] properties: content: type: string description: Full compose.yaml content. responses: "200": description: Stack saved. content: application/json: schema: $ref: "#/components/schemas/SuccessMessage" example: message: Stack saved successfully "400": description: Invalid stack name or content. content: application/json: schema: $ref: "#/components/schemas/Error" "403": $ref: "#/components/responses/Forbidden" "500": $ref: "#/components/responses/InternalError" delete: operationId: deleteStack tags: [Stacks] summary: Delete stack description: Permanently deletes the stack directory and all its files. Requires `stack:delete` permission. parameters: - $ref: "#/components/parameters/stackName" - $ref: "#/components/parameters/nodeId" responses: "200": description: Stack deleted. content: application/json: schema: $ref: "#/components/schemas/SuccessMessage" example: message: Stack deleted successfully "403": $ref: "#/components/responses/Forbidden" "500": $ref: "#/components/responses/InternalError" /api/stacks/{stackName}/envs: get: operationId: getStackEnvFiles tags: [Stacks] summary: List env file paths description: Returns the list of environment file paths referenced in the stack's compose file (`env_file` directives). parameters: - $ref: "#/components/parameters/stackName" - $ref: "#/components/parameters/nodeId" responses: "200": description: List of env file paths. content: application/json: schema: type: object required: [envFiles] properties: envFiles: type: array items: type: string example: [".env", "config/database.env"] "400": description: Invalid stack name. content: application/json: schema: $ref: "#/components/schemas/Error" "500": $ref: "#/components/responses/InternalError" /api/stacks/{stackName}/env-inventory: get: operationId: getStackEnvInventory tags: [Stacks] summary: Get environment inventory description: >- Returns a per-stack inventory of environment variables: each variable's source, whether Compose interpolates it or injects it into a container, and a status (present, missing, unused, duplicate, or unpersisted), plus likely-secret classification. Names only: a variable value is never read or returned. When the effective model cannot be rendered, `renderable` is false and the inventory is derived from the authored source alone. Requires `stack:read` permission. parameters: - $ref: "#/components/parameters/stackName" - $ref: "#/components/parameters/nodeId" responses: "200": description: The environment inventory. content: application/json: schema: type: object required: [stackName, renderable, items, envFiles, summary] properties: stackName: type: string renderable: type: boolean description: False when the effective model could not be rendered; the inventory is then partial. items: type: array items: type: object required: [key, sources, usedForInterpolation, injectedIntoService, required, hasDefault, likelySecret, status] properties: key: type: string sources: type: array items: type: string enum: [compose-inline, env-file, dotenv, process-env, compose-ref] usedForInterpolation: type: boolean injectedIntoService: type: boolean required: type: boolean hasDefault: type: boolean likelySecret: type: boolean status: type: string enum: [present, missing, unused, duplicate, unpersisted] envFiles: type: array items: type: object properties: rawPaths: type: array items: type: string existence: type: string enum: [present, missing, unverifiable] required: type: boolean isInterpolationSource: type: boolean isInjectionSource: type: boolean declaringServices: type: array items: type: string summary: type: object properties: total: { type: integer } missing: { type: integer } unused: { type: integer } duplicate: { type: integer } unpersisted: { type: integer } likelySecret: { type: integer } "403": $ref: "#/components/responses/Forbidden" "404": description: Stack not found. content: application/json: schema: $ref: "#/components/schemas/Error" "500": $ref: "#/components/responses/InternalError" /api/stacks/{stackName}/label-inventory: get: operationId: getStackLabelInventory tags: [Stacks] summary: Get Docker label inventory for a stack description: >- Returns declared Compose labels and runtime container labels per service, with reconciliation hints (only in Compose, only on running container, present in both, or value changed when the values differ) and provenance for each runtime label (compose, image, runtime, compose-system, or unknown when a container or image cannot be inspected). Secret-like label values are redacted unless the caller is an admin and passes `reveal=1`. Requires `stack:read` permission. parameters: - $ref: "#/components/parameters/stackName" - $ref: "#/components/parameters/nodeId" - name: reveal in: query required: false schema: type: string enum: ['1', 'true'] responses: "200": description: Stack label inventory. content: application/json: schema: $ref: "#/components/schemas/StackLabelInventory" "403": $ref: "#/components/responses/Forbidden" "404": description: Stack not found. content: application/json: schema: $ref: "#/components/schemas/Error" "500": $ref: "#/components/responses/InternalError" /api/system/container-labels: get: operationId: getNodeContainerLabels tags: [Fleet] summary: Get the Docker label inventory for the active node description: >- Returns every container on the node with its labels and provenance (compose, image, runtime, compose-system, or unknown), plus an inverted `byLabel` index. Marks `partial` when a container or image inspect fails. Secret-like values are redacted unless the caller is an admin and passes `reveal=1`. Requires `node:read` permission. parameters: - $ref: "#/components/parameters/nodeId" - name: reveal in: query required: false schema: type: string enum: ['1', 'true'] responses: "200": description: Node container label inventory. content: application/json: schema: $ref: "#/components/schemas/NodeLabelInventory" "403": $ref: "#/components/responses/Forbidden" "500": $ref: "#/components/responses/InternalError" /api/fleet/container-labels: get: operationId: getFleetContainerLabels tags: [Fleet] summary: Get the Docker label inventory aggregated across the fleet description: >- Fans out to every node's container label inventory and aggregates the inverted index by key, value, and source. Unreachable or malformed nodes degrade into `nodeErrors` rather than failing the whole request. Secret-like values are redacted unless the caller is an admin and passes `reveal=1`. Requires `node:read` permission. parameters: - name: reveal in: query required: false schema: type: string enum: ['1', 'true'] responses: "200": description: Fleet-wide container label inventory. content: application/json: schema: $ref: "#/components/schemas/FleetLabelInventory" "403": $ref: "#/components/responses/Forbidden" "500": $ref: "#/components/responses/InternalError" /api/stacks/{stackName}/env: get: operationId: getStackEnv tags: [Stacks] summary: Get env file content description: Returns the content of an environment file. Defaults to `.env` if no `file` query parameter is specified. parameters: - $ref: "#/components/parameters/stackName" - $ref: "#/components/parameters/nodeId" - name: file in: query required: false description: Path to a specific env file (must be in the resolved env files list). schema: type: string responses: "200": description: Raw env file content (KEY=VALUE format). content: text/plain: schema: type: string "400": description: Invalid stack name or disallowed file path. content: application/json: schema: $ref: "#/components/schemas/Error" "404": description: Env file not found. content: application/json: schema: $ref: "#/components/schemas/Error" example: error: Env file not found "500": $ref: "#/components/responses/InternalError" put: operationId: updateStackEnv tags: [Stacks] summary: Update env file description: Overwrites an environment file's content. Requires `stack:edit` permission. parameters: - $ref: "#/components/parameters/stackName" - $ref: "#/components/parameters/nodeId" - name: file in: query required: false description: Path to a specific env file to update. schema: type: string requestBody: required: true content: application/json: schema: type: object required: [content] properties: content: type: string description: Full env file content (KEY=VALUE format). responses: "200": description: Env file saved. content: application/json: schema: $ref: "#/components/schemas/SuccessMessage" example: message: Env file saved successfully "400": description: Invalid input. content: application/json: schema: $ref: "#/components/schemas/Error" "403": $ref: "#/components/responses/Forbidden" "500": $ref: "#/components/responses/InternalError" /api/stacks/{stackName}/containers: get: operationId: getStackContainers tags: [Stacks] summary: List stack containers description: Returns all containers belonging to the specified stack. parameters: - $ref: "#/components/parameters/stackName" - $ref: "#/components/parameters/nodeId" responses: "200": description: Array of container objects. content: application/json: schema: type: array items: $ref: "#/components/schemas/Container" "401": $ref: "#/components/responses/Unauthorized" "500": $ref: "#/components/responses/InternalError" /api/stacks/{stackName}/services: get: operationId: getStackServices tags: [Stacks] summary: List stack services description: Returns the service names defined in the stack's compose file. parameters: - $ref: "#/components/parameters/stackName" - $ref: "#/components/parameters/nodeId" responses: "200": description: Array of service names. content: application/json: schema: type: array items: type: string example: ["web", "db", "redis"] "400": description: Invalid stack name. content: application/json: schema: $ref: "#/components/schemas/Error" "500": $ref: "#/components/responses/InternalError" /api/stacks/{stackName}/deploy: post: operationId: deployStack tags: [Stacks] summary: Deploy stack description: | Runs `docker compose up -d` for the stack using atomic deployment with automatic rollback on failure. Requires `stack:deploy` permission. parameters: - $ref: "#/components/parameters/stackName" - $ref: "#/components/parameters/nodeId" responses: "200": description: Stack deployed successfully. content: application/json: schema: type: object required: [message] properties: message: type: string example: Deployed successfully healthGateId: type: string nullable: true description: | Id of the post-deploy health gate observation started for this deploy, for use with the health-gate endpoint. Null when the health gate is disabled on the node. "403": $ref: "#/components/responses/Forbidden" "500": description: Deployment failed. content: application/json: schema: type: object required: [error] properties: error: type: string rolledBack: type: boolean description: Whether the stack was automatically rolled back. failure: $ref: "#/components/schemas/FailureClassification" /api/stacks/{stackName}/down: post: operationId: downStack tags: [Stacks] summary: Tear down stack description: Runs `docker compose down` for the stack, removing containers and networks. Requires `stack:deploy` permission. parameters: - $ref: "#/components/parameters/stackName" - $ref: "#/components/parameters/nodeId" responses: "200": description: Command started. content: application/json: schema: type: object required: [status] properties: status: type: string example: Command started "403": $ref: "#/components/responses/Forbidden" "500": $ref: "#/components/responses/InternalError" /api/stacks/{stackName}/start: post: operationId: startStack tags: [Stacks] summary: Start stack containers description: Starts all stopped containers in the stack via the Docker Engine API. Requires `stack:deploy` permission. parameters: - $ref: "#/components/parameters/stackName" - $ref: "#/components/parameters/nodeId" responses: "200": description: Containers started. content: application/json: schema: $ref: "#/components/schemas/SuccessBoolean" "403": $ref: "#/components/responses/Forbidden" "404": description: No containers found for this stack. content: application/json: schema: $ref: "#/components/schemas/Error" "500": $ref: "#/components/responses/InternalError" /api/stacks/{stackName}/stop: post: operationId: stopStack tags: [Stacks] summary: Stop stack containers description: Stops all running containers in the stack via the Docker Engine API. Requires `stack:deploy` permission. parameters: - $ref: "#/components/parameters/stackName" - $ref: "#/components/parameters/nodeId" responses: "200": description: Containers stopped. content: application/json: schema: $ref: "#/components/schemas/SuccessBoolean" "403": $ref: "#/components/responses/Forbidden" "404": description: No containers found for this stack. content: application/json: schema: $ref: "#/components/schemas/Error" "500": $ref: "#/components/responses/InternalError" /api/stacks/{stackName}/restart: post: operationId: restartStack tags: [Stacks] summary: Restart stack containers description: Restarts all containers in the stack via the Docker Engine API. Requires `stack:deploy` permission. parameters: - $ref: "#/components/parameters/stackName" - $ref: "#/components/parameters/nodeId" responses: "200": description: Containers restarted. content: application/json: schema: $ref: "#/components/schemas/SuccessBoolean" "403": $ref: "#/components/responses/Forbidden" "404": description: No containers found for this stack. content: application/json: schema: $ref: "#/components/schemas/Error" "500": $ref: "#/components/responses/InternalError" /api/stacks/{stackName}/update: post: operationId: updateStackImages tags: [Stacks] summary: Pull and recreate stack description: | Pulls latest images and recreates containers (`docker compose pull && up -d`) using atomic update with automatic rollback on failure. Requires `stack:deploy` permission. parameters: - $ref: "#/components/parameters/stackName" - $ref: "#/components/parameters/nodeId" responses: "200": description: Update completed. content: application/json: schema: type: object required: [status] properties: status: type: string example: Update completed healthGateId: type: string nullable: true description: | Id of the post-update health gate observation started for this update, for use with the health-gate endpoint. Null when the health gate is disabled on the node. "403": $ref: "#/components/responses/Forbidden" "500": description: Update failed. content: application/json: schema: type: object required: [error] properties: error: type: string rolledBack: type: boolean failure: $ref: "#/components/schemas/FailureClassification" /api/stacks/{stackName}/rollback: post: operationId: rollbackStack tags: [Stacks] summary: Rollback stack description: Restores the stack to its previous deployment state. Requires `stack:deploy` permission. parameters: - $ref: "#/components/parameters/stackName" - $ref: "#/components/parameters/nodeId" responses: "200": description: Stack rolled back. content: application/json: schema: $ref: "#/components/schemas/SuccessMessage" example: message: 'Stack rolled back: compose and env files restored.' "403": $ref: "#/components/responses/Forbidden" "404": description: No backup available. content: application/json: schema: $ref: "#/components/schemas/Error" example: error: No backup available for this stack. "500": $ref: "#/components/responses/InternalError" /api/stacks/{stackName}/backup: get: operationId: getStackBackupStatus tags: [Stacks] summary: Check backup status description: Returns whether a deployment backup exists for this stack and when it was created. parameters: - $ref: "#/components/parameters/stackName" - $ref: "#/components/parameters/nodeId" responses: "200": description: Backup status. content: application/json: schema: type: object required: [exists] properties: exists: type: boolean timestamp: type: ["integer", "null"] description: Unix timestamp of the backup, or null if no backup exists. "401": $ref: "#/components/responses/Unauthorized" "500": $ref: "#/components/responses/InternalError" /api/stacks/{stackName}/update-readiness: get: operationId: getStackUpdateReadiness tags: [Stacks] summary: Check update readiness description: | Computes an advisory readiness verdict for updating the stack right now, from the stored preflight result, open drift findings, live container health, the pending image change, the rollback backup slot, and node disk headroom. Advisory only: no verdict blocks an update. Requires `stack:read` permission. parameters: - $ref: "#/components/parameters/stackName" - $ref: "#/components/parameters/nodeId" responses: "200": description: Readiness report. content: application/json: schema: type: object required: [stack, computedAt, verdict, signals] properties: stack: type: string computedAt: type: integer description: Unix timestamp (ms) the report was computed. verdict: type: string enum: [ready, ready_with_warnings, review_required, blocked, unknown] signals: type: array items: type: object required: [id, status, title, detail, affectsVerdict] properties: id: type: string status: type: string enum: [ok, warning, attention, blocked, unknown] title: type: string detail: type: string affectsVerdict: type: boolean "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "500": $ref: "#/components/responses/InternalError" /api/stacks/{stackName}/rollback-readiness: get: operationId: getStackRollbackReadiness tags: [Stacks] summary: Check rollback readiness description: | Reports what a rollback of this stack can actually restore: the backed up compose file, env coverage (variable names only, never values), the known previous image tag, and an explicit disclosure that volume and bind-mounted data are not covered by file backups. Requires `stack:read` permission. parameters: - $ref: "#/components/parameters/stackName" - $ref: "#/components/parameters/nodeId" responses: "200": description: Rollback readiness report. content: application/json: schema: type: object required: [stack, computedAt, overall, items] properties: stack: type: string computedAt: type: integer overall: type: string enum: [ready, partial, not_ready] items: type: array items: type: object required: [id, state, label, detail] properties: id: type: string state: type: string enum: [ready, missing, unknown, not_covered] label: type: string detail: type: string "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "500": $ref: "#/components/responses/InternalError" /api/stacks/{stackName}/health-gate: get: operationId: getStackHealthGate tags: [Stacks] summary: Get post-update health gate result description: | Returns a post-deploy health gate observation for the stack: the run named by `gateId` (as returned in a deploy or update response), or the most recent run when omitted. Status `never-run` means no observation exists. Requires `stack:read` permission. parameters: - $ref: "#/components/parameters/stackName" - $ref: "#/components/parameters/nodeId" - name: gateId in: query required: false schema: type: string description: A specific gate run id to read instead of the latest. responses: "200": description: Health gate report. content: application/json: schema: type: object required: [stack, status] properties: stack: type: string id: type: string nullable: true status: type: string enum: [observing, passed, failed, unknown, never-run] trigger: type: string nullable: true enum: [update, deploy, null] reason: type: string nullable: true windowSeconds: type: integer nullable: true startedAt: type: integer nullable: true endedAt: type: integer nullable: true containers: type: array items: type: object required: [name, state, restarts] properties: name: type: string state: type: string description: Docker container state, or `missing` when it vanished mid-observation. health: type: string nullable: true restarts: type: integer "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "500": $ref: "#/components/responses/InternalError" # ── Containers ────────────────────────────────────────── /api/containers: get: operationId: listContainers tags: [Containers] summary: List all containers description: Returns all running containers on the target node. parameters: - $ref: "#/components/parameters/nodeId" - $ref: "#/components/parameters/nodeIdQuery" responses: "200": description: Array of container objects. content: application/json: schema: type: array items: $ref: "#/components/schemas/Container" "401": $ref: "#/components/responses/Unauthorized" "500": $ref: "#/components/responses/InternalError" /api/containers/{id}/logs: get: operationId: streamContainerLogs tags: [Containers] summary: Stream container logs description: | Streams container logs as Server-Sent Events (SSE). The connection remains open and pushes new log lines as `data:` events in real time. parameters: - $ref: "#/components/parameters/idPath" - $ref: "#/components/parameters/nodeId" responses: "200": description: SSE log stream. content: text/event-stream: schema: type: string description: Each event is a `data:` line containing a JSON-encoded log string. "401": $ref: "#/components/responses/Unauthorized" "500": $ref: "#/components/responses/InternalError" /api/containers/{id}/start: post: operationId: startContainer tags: [Containers] summary: Start container description: Starts a stopped container. Requires admin role. parameters: - $ref: "#/components/parameters/idPath" - $ref: "#/components/parameters/nodeId" responses: "200": description: Container started. content: application/json: schema: $ref: "#/components/schemas/SuccessMessage" example: message: Container started "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "500": $ref: "#/components/responses/InternalError" /api/containers/{id}/stop: post: operationId: stopContainer tags: [Containers] summary: Stop container description: Stops a running container. Requires admin role. parameters: - $ref: "#/components/parameters/idPath" - $ref: "#/components/parameters/nodeId" responses: "200": description: Container stopped. content: application/json: schema: $ref: "#/components/schemas/SuccessMessage" example: message: Container stopped "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "500": $ref: "#/components/responses/InternalError" /api/containers/{id}/restart: post: operationId: restartContainer tags: [Containers] summary: Restart container description: Restarts a container. Requires admin role. parameters: - $ref: "#/components/parameters/idPath" - $ref: "#/components/parameters/nodeId" responses: "200": description: Container restarted. content: application/json: schema: $ref: "#/components/schemas/SuccessMessage" example: message: Container restarted "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "500": $ref: "#/components/responses/InternalError" # ── API Tokens ────────────────────────────────────────── /api/api-tokens: post: operationId: createApiToken tags: [API Tokens] summary: Create API token description: | Generates a new scoped API token. The full token is only returned in the creation response and cannot be retrieved again. Requires admin role. **Note:** API tokens cannot create other API tokens. responses: "201": description: Token created. The `token` field contains the full JWT. Save it now; it will not be shown again. content: application/json: schema: type: object required: [id, token] properties: id: type: integer token: type: string description: Full JWT token. Store securely — this is the only time it's returned. "400": description: Validation error (missing/invalid fields, or maximum of 25 active tokens reached). content: application/json: schema: $ref: "#/components/schemas/Error" "403": $ref: "#/components/responses/Forbidden" "409": description: An active token with this name already exists. content: application/json: schema: $ref: "#/components/schemas/Error" "500": $ref: "#/components/responses/InternalError" requestBody: required: true content: application/json: schema: type: object required: [name, scope] properties: name: type: string description: Human-readable token name. Must be unique among the user's active tokens. maxLength: 100 example: CI/CD Deploy Token scope: type: string description: | Permission scope: - `read-only` — GET requests only - `deploy-only` — Read + stack deploy/stop/restart operations - `full-admin` — All operations (except license, user, and token management) enum: [read-only, deploy-only, full-admin] expires_in: type: ["integer", "null"] description: Token lifetime in days. Use `null` for no expiry. enum: [30, 60, 90, 365, null] example: 90 get: operationId: listApiTokens tags: [API Tokens] summary: List API tokens description: | Returns all API tokens for the current user. Token hashes are never exposed. Requires admin role. **Note:** API tokens cannot list other API tokens. responses: "200": description: Array of token metadata. content: application/json: schema: type: array items: $ref: "#/components/schemas/ApiToken" "403": $ref: "#/components/responses/Forbidden" "500": $ref: "#/components/responses/InternalError" /api/api-tokens/{id}: delete: operationId: revokeApiToken tags: [API Tokens] summary: Revoke API token description: | Permanently revokes an API token. Users can only revoke their own tokens. Requires admin role. parameters: - $ref: "#/components/parameters/idPath" responses: "200": description: Token revoked. content: application/json: schema: $ref: "#/components/schemas/SuccessBoolean" "400": description: Invalid token ID. content: application/json: schema: $ref: "#/components/schemas/Error" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "500": $ref: "#/components/responses/InternalError" # ── Webhooks ──────────────────────────────────────────── /api/webhooks: get: operationId: listWebhooks tags: [Webhooks] summary: List webhooks description: Returns all configured webhooks with masked secrets. responses: "200": description: Array of webhook objects. content: application/json: schema: type: array items: $ref: "#/components/schemas/Webhook" "403": $ref: "#/components/responses/Forbidden" "500": $ref: "#/components/responses/InternalError" post: operationId: createWebhook tags: [Webhooks] summary: Create webhook description: | Creates a new webhook for a stack. The webhook secret is auto-generated and only returned in the creation response. Requires admin role. requestBody: required: true content: application/json: schema: type: object required: [name, stack_name, action] properties: name: type: string maxLength: 100 example: GitHub Deploy Hook stack_name: type: string description: Target stack name. example: my-app action: type: string enum: [deploy, restart, stop, start, pull, git-pull] description: Action to execute when the webhook is triggered. `git-pull` requires a Git source attached to the stack. enabled: type: boolean default: true node_id: type: integer description: Node the webhook executes against. Defaults to the request's resolved node or the default node. responses: "201": description: Webhook created. The `secret` field is the HMAC signing key; save it now. content: application/json: schema: type: object required: [id, secret] properties: id: type: integer secret: type: string description: HMAC-SHA256 signing secret. Store securely; this is the only time it's returned in full. "400": description: Validation error. content: application/json: schema: $ref: "#/components/schemas/Error" "403": $ref: "#/components/responses/Forbidden" "500": $ref: "#/components/responses/InternalError" /api/webhooks/{id}: put: operationId: updateWebhook tags: [Webhooks] summary: Update webhook description: Updates webhook configuration. Requires admin role. parameters: - $ref: "#/components/parameters/idPath" requestBody: required: true content: application/json: schema: type: object properties: name: type: string maxLength: 100 stack_name: type: string action: type: string enum: [deploy, restart, stop, start, pull, git-pull] enabled: type: boolean node_id: type: integer description: Retarget the webhook to a different node. responses: "200": description: Webhook updated. content: application/json: schema: $ref: "#/components/schemas/SuccessBoolean" "400": description: Validation error. content: application/json: schema: $ref: "#/components/schemas/Error" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "500": $ref: "#/components/responses/InternalError" delete: operationId: deleteWebhook tags: [Webhooks] summary: Delete webhook description: Permanently deletes a webhook. Requires admin role. parameters: - $ref: "#/components/parameters/idPath" responses: "200": description: Webhook deleted. content: application/json: schema: $ref: "#/components/schemas/SuccessBoolean" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "500": $ref: "#/components/responses/InternalError" /api/webhooks/{id}/history: get: operationId: getWebhookHistory tags: [Webhooks] summary: Get webhook execution history description: Returns the execution log for a webhook. parameters: - $ref: "#/components/parameters/idPath" responses: "200": description: Array of execution records. content: application/json: schema: type: array items: $ref: "#/components/schemas/WebhookExecution" "403": $ref: "#/components/responses/Forbidden" "500": $ref: "#/components/responses/InternalError" /api/webhooks/{id}/trigger: post: operationId: triggerWebhook tags: [Webhooks] summary: Trigger webhook description: | Externally triggers a webhook action. This endpoint is public but requires a valid HMAC-SHA256 signature in the `X-Webhook-Signature` header. Sign the exact raw bytes of the request body; an empty body is rejected. Compute the signature as: `sha256=` + HMAC-SHA256(raw_request_body, webhook_secret). Every unauthenticated rejection (unknown id, disabled webhook, non-paid licence, missing or invalid signature, empty body) returns the same `404` response so callers cannot enumerate webhook ids or fingerprint the instance's licence tier. security: - webhookSignature: [] parameters: - $ref: "#/components/parameters/idPath" requestBody: required: true content: application/json: schema: type: object properties: action: type: string enum: [deploy, restart, stop, start, pull, git-pull] description: Override the default webhook action. Must be one of the allowed actions. responses: "202": description: Webhook accepted and action queued. content: application/json: schema: type: object required: [message, action] properties: message: type: string example: Webhook accepted action: type: string example: deploy "400": description: Authentication succeeded but the body's `action` override was not in the allowlist. content: application/json: schema: $ref: "#/components/schemas/Error" "404": description: Authentication failed. The webhook is unknown, disabled, the licence is not paid, the signature header is missing, the body was empty, or the signature did not match. Sencho returns the same response for every unauthenticated case. content: application/json: schema: $ref: "#/components/schemas/Error" "429": description: Rate limit exceeded. The trigger endpoint allows 500 requests per minute per source IP; CI/CD callers behind shared NAT may share that budget. content: application/json: schema: $ref: "#/components/schemas/Error" "500": $ref: "#/components/responses/InternalError" # ── Nodes ─────────────────────────────────────────────── /api/nodes: get: operationId: listNodes tags: [Nodes] summary: List all nodes description: Returns all registered nodes (local and remote). responses: "200": description: Array of node objects. content: application/json: schema: type: array items: $ref: "#/components/schemas/Node" "401": $ref: "#/components/responses/Unauthorized" "500": $ref: "#/components/responses/InternalError" post: operationId: createNode tags: [Nodes] summary: Register a new node description: | Adds a new local or remote node. Only one local node is allowed per instance; attempting to create a second local node returns 409. Remote nodes require an API URL pointing to another Sencho instance and an API token for authentication. Requires `node:manage` permission. Node type is immutable after creation. **Note:** API tokens cannot manage nodes. requestBody: required: true content: application/json: schema: type: object required: [name, type] properties: name: type: string example: production-server type: type: string enum: [local, remote] compose_dir: type: string description: Base directory for Compose stacks (defaults to `/app/compose`). is_default: type: boolean description: Set as the default node. api_url: type: string description: Remote Sencho instance URL (required for remote nodes). example: https://sencho.example.com:1852 api_token: type: string description: API token for authenticating with the remote Sencho instance. responses: "200": description: Node created. content: application/json: schema: type: object required: [success, id] properties: success: type: boolean id: type: integer description: New node ID. warning: type: string description: Warning if the remote URL uses plain HTTP. "400": description: Validation error. content: application/json: schema: $ref: "#/components/schemas/Error" "403": $ref: "#/components/responses/Forbidden" "409": description: Node name already exists, or a local node already exists. content: application/json: schema: $ref: "#/components/schemas/Error" "500": $ref: "#/components/responses/InternalError" /api/nodes/{id}: get: operationId: getNode tags: [Nodes] summary: Get node details parameters: - $ref: "#/components/parameters/idPath" responses: "200": description: Node object. content: application/json: schema: $ref: "#/components/schemas/Node" "404": $ref: "#/components/responses/NotFound" "500": $ref: "#/components/responses/InternalError" put: operationId: updateNode tags: [Nodes] summary: Update node description: Updates node configuration. Node type is immutable after creation. Requires `node:manage` permission. parameters: - $ref: "#/components/parameters/idPath" requestBody: required: true content: application/json: schema: type: object properties: name: type: string compose_dir: type: string is_default: type: boolean api_url: type: string api_token: type: string responses: "200": description: Node updated. content: application/json: schema: type: object required: [success] properties: success: type: boolean warning: type: string "400": description: Validation error. content: application/json: schema: $ref: "#/components/schemas/Error" "403": $ref: "#/components/responses/Forbidden" "500": $ref: "#/components/responses/InternalError" delete: operationId: deleteNode tags: [Nodes] summary: Delete node description: Removes a node from the fleet. The last local node cannot be deleted. Requires `node:manage` permission. parameters: - $ref: "#/components/parameters/idPath" responses: "200": description: Node deleted. content: application/json: schema: $ref: "#/components/schemas/SuccessBoolean" "400": description: Cannot delete the only local node (or the default node). content: application/json: schema: $ref: "#/components/schemas/Error" "403": $ref: "#/components/responses/Forbidden" "500": $ref: "#/components/responses/InternalError" /api/nodes/{id}/test: post: operationId: testNodeConnection tags: [Nodes] summary: Test node connection description: Tests connectivity to a node. For remote nodes, pings the remote Sencho instance's health endpoint. parameters: - $ref: "#/components/parameters/idPath" responses: "200": description: Connection test result. content: application/json: schema: type: object required: [success] properties: success: type: boolean message: type: string error: type: string "401": $ref: "#/components/responses/Unauthorized" "500": $ref: "#/components/responses/InternalError" /api/nodes/{id}/meta: get: operationId: getNodeMeta tags: [Nodes] summary: Get node capabilities description: "Returns the version and supported capabilities for a specific node. For local nodes, returns this instance's own metadata. For remote nodes, relays the response from the remote instance's `/api/meta` endpoint. Returns `{ version: null, capabilities: [] }` if the remote node is offline or doesn't support the meta endpoint." parameters: - $ref: "#/components/parameters/idPath" responses: "200": description: Node capability metadata. content: application/json: schema: type: object required: [version, capabilities] properties: version: type: string nullable: true description: Sencho version of the node, or null if unavailable. example: "0.31.0" capabilities: type: array description: List of feature capabilities the node supports. Empty array if unavailable. items: type: string example: ["stacks", "containers", "fleet"] "401": $ref: "#/components/responses/Unauthorized" "404": $ref: "#/components/responses/NotFound" # ── Fleet ─────────────────────────────────────────────── /api/fleet/overview: get: operationId: getFleetOverview tags: [Fleet] summary: Fleet overview description: Returns an aggregated overview of all nodes including status, container stats, and stack counts. responses: "200": description: Array of fleet node overviews. content: application/json: schema: type: array items: $ref: "#/components/schemas/FleetNodeOverview" "401": $ref: "#/components/responses/Unauthorized" "500": $ref: "#/components/responses/InternalError" /api/fleet/node/{nodeId}/stacks: get: operationId: getFleetNodeStacks tags: [Fleet] summary: List stacks on a fleet node description: Returns stack names from a specific fleet node. parameters: - name: nodeId in: path required: true schema: type: integer responses: "200": description: Array of stack names. content: application/json: schema: type: array items: type: string "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "500": $ref: "#/components/responses/InternalError" "502": description: Failed to reach remote node. content: application/json: schema: $ref: "#/components/schemas/Error" "503": description: Remote node not configured. content: application/json: schema: $ref: "#/components/schemas/Error" /api/fleet/node/{nodeId}/stacks/{stackName}/containers: get: operationId: getFleetNodeStackContainers tags: [Fleet] summary: List containers in a fleet node stack description: Returns containers for a specific stack on a specific fleet node. parameters: - name: nodeId in: path required: true schema: type: integer - $ref: "#/components/parameters/stackName" responses: "200": description: Array of container objects. content: application/json: schema: type: array items: $ref: "#/components/schemas/Container" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "500": $ref: "#/components/responses/InternalError" "503": description: Remote node not configured. content: application/json: schema: $ref: "#/components/schemas/Error" /api/fleet/update-status: get: operationId: getFleetUpdateStatus tags: [Fleet] summary: Fleet update status description: Returns version comparison and active update status for all nodes. Compares each node's version against the gateway's version. responses: "200": description: Update status for all nodes. content: application/json: schema: type: object properties: nodes: type: array items: type: object properties: nodeId: type: integer name: type: string type: type: string enum: [local, remote] version: type: string nullable: true latestVersion: type: string updateAvailable: type: boolean updateStatus: type: string nullable: true enum: [updating, completed, timeout, failed, null] skipActive: type: boolean skippedVersion: type: string nullable: true "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" /api/fleet/nodes/{nodeId}/skip-version: post: operationId: skipNodeVersion tags: [Fleet] summary: Skip a node version description: Marks a version as skipped for a node, hiding the update prompt and excluding it from update-all. Admin only. parameters: - name: nodeId in: path required: true schema: type: integer requestBody: required: true content: application/json: schema: type: object required: [version] properties: version: type: string description: Semver version string to skip. responses: "204": description: Skip recorded. "400": description: Invalid parameters. "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": description: Node not found. delete: operationId: unskipNodeVersion tags: [Fleet] summary: Clear a node version skip description: Removes the skipped-version record for a node. Admin only. parameters: - name: nodeId in: path required: true schema: type: integer responses: "204": description: Skip cleared. "400": description: Invalid parameters. "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": description: Node not found. /api/fleet/update-status/release-notes: get: operationId: getFleetReleaseNotes tags: [Fleet] summary: Release notes for the latest Sencho version description: Returns the GitHub release body and URL for the latest Sencho release, used by the Changelog tab. responses: "200": description: Release notes. content: application/json: schema: type: object properties: releaseNotes: type: string nullable: true htmlUrl: type: string nullable: true "401": $ref: "#/components/responses/Unauthorized" /api/fleet/nodes/{nodeId}/update: post: operationId: triggerNodeUpdate tags: [Fleet] summary: Trigger node update description: Initiates a self-update on the specified node. For remote nodes, sends the update command via the proxy. For local nodes, triggers the update directly. parameters: - name: nodeId in: path required: true schema: type: integer responses: "202": description: Update initiated. content: application/json: schema: type: object properties: message: type: string "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": description: Node not found. content: application/json: schema: $ref: "#/components/schemas/Error" "409": description: Update already in progress. content: application/json: schema: $ref: "#/components/schemas/Error" "503": description: Self-update not available on the target node. content: application/json: schema: $ref: "#/components/schemas/Error" /api/fleet/update-all: post: operationId: triggerFleetUpdateAll tags: [Fleet] summary: Update all outdated nodes description: Triggers self-update on all remote nodes running an older version than the gateway. Local nodes are excluded from bulk updates. responses: "202": description: Bulk update initiated. content: application/json: schema: type: object properties: updating: type: array items: type: string description: Names of nodes where update was triggered. skipped: type: array items: type: string description: Names of nodes that were skipped. "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" /api/fleet/snapshots: post: operationId: createFleetSnapshot tags: [Fleet] summary: Create fleet snapshot description: Creates a point-in-time backup of all compose files across all nodes. Requires admin role. requestBody: required: false content: application/json: schema: type: object properties: description: type: string description: Optional description for the snapshot. example: Pre-migration backup responses: "201": description: Snapshot created. content: application/json: schema: $ref: "#/components/schemas/FleetSnapshot" "403": $ref: "#/components/responses/Forbidden" "500": $ref: "#/components/responses/InternalError" get: operationId: listFleetSnapshots tags: [Fleet] summary: List fleet snapshots description: Returns paginated fleet snapshots. parameters: - name: limit in: query schema: type: integer default: 50 maximum: 100 - name: offset in: query schema: type: integer default: 0 responses: "200": description: Paginated snapshot list. content: application/json: schema: type: object required: [snapshots, total] properties: snapshots: type: array items: $ref: "#/components/schemas/FleetSnapshot" total: type: integer "403": $ref: "#/components/responses/Forbidden" "500": $ref: "#/components/responses/InternalError" /api/fleet/snapshots/{id}: get: operationId: getFleetSnapshot tags: [Fleet] summary: Get snapshot details description: Returns full snapshot details including all captured files grouped by node and stack. parameters: - $ref: "#/components/parameters/idPath" responses: "200": description: Snapshot with file contents. content: application/json: schema: $ref: "#/components/schemas/FleetSnapshotDetail" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "500": $ref: "#/components/responses/InternalError" delete: operationId: deleteFleetSnapshot tags: [Fleet] summary: Delete snapshot description: Permanently deletes a fleet snapshot. Requires admin role. parameters: - $ref: "#/components/parameters/idPath" responses: "200": description: Snapshot deleted. content: application/json: schema: $ref: "#/components/schemas/SuccessBoolean" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "500": $ref: "#/components/responses/InternalError" /api/fleet/snapshots/{id}/restore: post: operationId: restoreFleetSnapshot tags: [Fleet] summary: Restore from snapshot description: Restores a specific stack on a specific node from the snapshot. Optionally redeploys after restore. Requires admin role. parameters: - $ref: "#/components/parameters/idPath" requestBody: required: true content: application/json: schema: type: object required: [nodeId, stackName] properties: nodeId: type: integer description: Target node ID to restore to. stackName: type: string description: Stack to restore. redeploy: type: boolean default: false description: Whether to redeploy the stack after restoring files. responses: "200": description: Stack restored. content: application/json: schema: $ref: "#/components/schemas/SuccessMessage" example: message: Stack restored successfully. "400": description: Missing required fields. content: application/json: schema: $ref: "#/components/schemas/Error" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "500": $ref: "#/components/responses/InternalError" # ── Scheduled Tasks ───────────────────────────────────── /api/scheduled-tasks: get: operationId: listScheduledTasks tags: [Scheduled Tasks] summary: List scheduled tasks description: Returns all scheduled tasks. Requires admin role. responses: "200": description: Array of scheduled task objects. content: application/json: schema: type: array items: $ref: "#/components/schemas/ScheduledTask" "403": $ref: "#/components/responses/Forbidden" "500": $ref: "#/components/responses/InternalError" post: operationId: createScheduledTask tags: [Scheduled Tasks] summary: Create scheduled task description: | Creates a new recurring task. Action-target rules: - `restart` requires `target_type: stack` or `target_type: container` (with `target_id` and `node_id`) - `auto_stop` and `auto_start` accept `target_type: stack` or `target_type: container` - `snapshot` requires `target_type: fleet` - `prune` requires `target_type: system` Requires admin role. requestBody: required: true content: application/json: schema: type: object required: [name, target_type, action, cron_expression] properties: name: type: string example: Nightly restart target_type: type: string enum: [stack, fleet, system, container] target_id: type: string description: Stack or container name (required when target_type is `stack` or `container`). node_id: type: integer description: Target node ID (required when target_type is `stack` or `container`). action: type: string enum: [restart, snapshot, prune] cron_expression: type: string description: Standard 5-field cron expression. example: "0 3 * * *" enabled: type: boolean default: true prune_targets: type: array items: type: string enum: [containers, images, networks, volumes] description: Resources to prune (only for `prune` action). target_services: type: array items: type: string description: Specific services to restart (only for `restart` action). prune_label_filter: type: string description: Docker label filter for prune operations. responses: "201": description: Task created. content: application/json: schema: $ref: "#/components/schemas/ScheduledTask" "400": description: Validation error. content: application/json: schema: $ref: "#/components/schemas/Error" "403": $ref: "#/components/responses/Forbidden" "500": $ref: "#/components/responses/InternalError" /api/scheduled-tasks/{id}: get: operationId: getScheduledTask tags: [Scheduled Tasks] summary: Get scheduled task parameters: - $ref: "#/components/parameters/idPath" responses: "200": description: Scheduled task object. content: application/json: schema: $ref: "#/components/schemas/ScheduledTask" "400": description: Invalid task ID. content: application/json: schema: $ref: "#/components/schemas/Error" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "500": $ref: "#/components/responses/InternalError" put: operationId: updateScheduledTask tags: [Scheduled Tasks] summary: Update scheduled task description: Updates task configuration. Same validation rules as creation apply. Requires admin role. parameters: - $ref: "#/components/parameters/idPath" requestBody: required: true content: application/json: schema: type: object properties: name: type: string target_type: type: string enum: [stack, fleet, system, container] target_id: type: string description: Stack or container name when target_type is `stack` or `container`. node_id: type: integer action: type: string enum: [restart, snapshot, prune] cron_expression: type: string enabled: type: boolean prune_targets: type: array items: type: string target_services: type: array items: type: string prune_label_filter: type: string responses: "200": description: Task updated. content: application/json: schema: $ref: "#/components/schemas/ScheduledTask" "400": description: Validation error. content: application/json: schema: $ref: "#/components/schemas/Error" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "500": $ref: "#/components/responses/InternalError" delete: operationId: deleteScheduledTask tags: [Scheduled Tasks] summary: Delete scheduled task description: Permanently deletes a scheduled task. Requires admin role. parameters: - $ref: "#/components/parameters/idPath" responses: "200": description: Task deleted. content: application/json: schema: $ref: "#/components/schemas/SuccessBoolean" "400": description: Invalid task ID. content: application/json: schema: $ref: "#/components/schemas/Error" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "500": $ref: "#/components/responses/InternalError" /api/scheduled-tasks/{id}/toggle: patch: operationId: toggleScheduledTask tags: [Scheduled Tasks] summary: Toggle task enabled/disabled description: Flips the enabled state of a scheduled task. Requires admin role. parameters: - $ref: "#/components/parameters/idPath" responses: "200": description: Task toggled. Returns updated task. content: application/json: schema: $ref: "#/components/schemas/ScheduledTask" "400": description: Invalid task ID. content: application/json: schema: $ref: "#/components/schemas/Error" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "500": $ref: "#/components/responses/InternalError" /api/scheduled-tasks/{id}/run: post: operationId: runScheduledTask tags: [Scheduled Tasks] summary: Run task immediately description: Executes the scheduled task immediately, regardless of its cron schedule. Requires admin role. parameters: - $ref: "#/components/parameters/idPath" responses: "200": description: Task executed. Returns updated task with `last_run_at`. content: application/json: schema: $ref: "#/components/schemas/ScheduledTask" "400": description: Invalid task ID. content: application/json: schema: $ref: "#/components/schemas/Error" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "500": $ref: "#/components/responses/InternalError" /api/scheduled-tasks/{id}/runs: get: operationId: listScheduledTaskRuns tags: [Scheduled Tasks] summary: List task execution history description: Returns paginated execution history for a scheduled task. Requires admin role. parameters: - $ref: "#/components/parameters/idPath" - name: limit in: query schema: type: integer default: 50 - name: offset in: query schema: type: integer default: 0 responses: "200": description: Array of task run records. content: application/json: schema: type: array items: $ref: "#/components/schemas/ScheduledTaskRun" "400": description: Invalid task ID. content: application/json: schema: $ref: "#/components/schemas/Error" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "500": $ref: "#/components/responses/InternalError" /api/scheduled-tasks/{id}/runs/export: get: operationId: exportScheduledTaskRuns tags: [Scheduled Tasks] summary: Export task history as CSV description: Downloads the execution history for a scheduled task as a CSV file. Requires admin role. parameters: - $ref: "#/components/parameters/idPath" responses: "200": description: CSV file download. content: text/csv: schema: type: string description: "CSV with columns: Timestamp, Source, Status, Duration (s), Details" "400": description: Invalid task ID. content: application/json: schema: $ref: "#/components/schemas/Error" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "500": $ref: "#/components/responses/InternalError" # ── Registries ────────────────────────────────────────── /api/registries: get: operationId: listRegistries tags: [Registries] summary: List registries description: | Returns all configured private container registries. Secrets are never included in the response. Requires Admiral license and admin role. API tokens receive `SCOPE_DENIED`. responses: "200": description: Array of registry objects. content: application/json: schema: type: array items: $ref: "#/components/schemas/Registry" "403": $ref: "#/components/responses/Forbidden" "500": $ref: "#/components/responses/InternalError" post: operationId: createRegistry tags: [Registries] summary: Create registry description: | Adds a new private container registry. The secret is encrypted at rest using AES-256-GCM. Requires Admiral license and admin role. API tokens receive `SCOPE_DENIED`. requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/RegistryCreate" responses: "201": description: Registry created. content: application/json: schema: type: object required: [id] properties: id: type: integer "400": description: Validation error. content: application/json: schema: $ref: "#/components/schemas/Error" "403": $ref: "#/components/responses/Forbidden" "500": $ref: "#/components/responses/InternalError" /api/registries/{id}: put: operationId: updateRegistry tags: [Registries] summary: Update registry description: | Updates an existing registry. All fields are optional — only provided fields are changed. Requires Admiral license and admin role. API tokens receive `SCOPE_DENIED`. parameters: - $ref: "#/components/parameters/idPath" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/RegistryCreate" responses: "200": description: Registry updated. content: application/json: schema: $ref: "#/components/schemas/SuccessBoolean" "400": description: Validation error. content: application/json: schema: $ref: "#/components/schemas/Error" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "500": $ref: "#/components/responses/InternalError" delete: operationId: deleteRegistry tags: [Registries] summary: Delete registry description: | Removes a registry and its encrypted credentials. Requires Admiral license and admin role. API tokens receive `SCOPE_DENIED`. parameters: - $ref: "#/components/parameters/idPath" responses: "200": description: Registry deleted. content: application/json: schema: $ref: "#/components/schemas/SuccessBoolean" "400": description: Invalid registry ID. content: application/json: schema: $ref: "#/components/schemas/Error" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "500": $ref: "#/components/responses/InternalError" /api/registries/{id}/test: post: operationId: testRegistry tags: [Registries] summary: Test registry connection description: | Tests connectivity and authentication against a configured registry. Requires Admiral license and admin role. API tokens receive `SCOPE_DENIED`. parameters: - $ref: "#/components/parameters/idPath" responses: "200": description: Test result. content: application/json: schema: type: object properties: success: type: boolean message: type: string "400": description: Invalid registry ID. content: application/json: schema: $ref: "#/components/schemas/Error" "403": $ref: "#/components/responses/Forbidden" "500": $ref: "#/components/responses/InternalError" # ── Image Updates ─────────────────────────────────────── /api/image-updates: get: operationId: getImageUpdates tags: [Image Updates] summary: Get image update status description: Returns the update availability status for all stacks. responses: "200": description: Array of update status objects per stack. content: application/json: schema: type: array items: $ref: "#/components/schemas/ImageUpdateStatus" "401": $ref: "#/components/responses/Unauthorized" "500": $ref: "#/components/responses/InternalError" /api/image-updates/refresh: post: operationId: refreshImageUpdates tags: [Image Updates] summary: Trigger image update check description: | Triggers a background check for newer image versions across all stacks. Rate limited to one refresh per 10 minutes. Requires admin role. responses: "200": description: Refresh started. content: application/json: schema: type: object properties: success: type: boolean message: type: string example: Image update check started in background. "401": $ref: "#/components/responses/Unauthorized" "429": description: Rate limited — wait at least 10 minutes between refreshes. content: application/json: schema: $ref: "#/components/schemas/Error" "500": $ref: "#/components/responses/InternalError" /api/image-updates/status: get: operationId: getImageUpdateCheckStatus tags: [Image Updates] summary: Get scanner cadence status description: Returns the scanner's current status including scheduling mode and next check time. responses: "200": description: Scanner status. content: application/json: schema: $ref: "#/components/schemas/ImageUpdateCheckStatus" "401": $ref: "#/components/responses/Unauthorized" /api/image-updates/interval: put: operationId: setImageUpdateCheckSchedule tags: [Image Updates] summary: Set the detection schedule description: Configure the image-update check cadence. In interval mode only `minutes` is required. In cron mode also send `mode` and `cron`. requestBody: required: true content: application/json: schema: type: object required: [minutes] properties: minutes: type: integer minimum: 15 maximum: 1440 description: Fallback interval in minutes (used in interval mode, or as fallback when cron is unset). mode: type: string enum: [interval, cron] description: Scheduling mode. Omit to leave unchanged. cron: type: string description: 5-field cron expression (required when mode is 'cron'). Nicknames like @daily are accepted. responses: "200": description: Schedule updated. content: application/json: schema: $ref: "#/components/schemas/ImageUpdateCheckStatus" "400": $ref: "#/components/responses/Forbidden" "401": $ref: "#/components/responses/Unauthorized" "403": description: Non-admin users cannot change the schedule.