{
  "openapi": "3.0.3",
  "info": {
    "title": "xhost API",
    "description": "Deploy websites and apps from your terminal. xhost is a hosting platform that deploys static sites and dynamic applications via a git-based workflow.",
    "version": "1.0.0",
    "contact": {
      "name": "xhost",
      "url": "https://docs.xhostd.com"
    }
  },
  "servers": [
    {
      "url": "https://api.xhostd.com",
      "description": "Production"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "paths": {
    "/apps": {
      "get": {
        "operationId": "listApps",
        "summary": "List apps",
        "description": "List all apps owned by the authenticated user.",
        "tags": ["Apps"],
        "responses": {
          "200": {
            "description": "List of apps",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AppListResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createApp",
        "summary": "Create app",
        "description": "Create a new app. Provisions a git repo and a prod channel. Requires repo:* scope.",
        "tags": ["Apps"],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateAppRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "App created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AppResponse"
                }
              }
            }
          },
          "400": {
            "description": "Invalid name, reserved prefix, name taken, or invalid template",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "Token lacks repo:* scope",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/apps/{app_id}": {
      "parameters": [
        {
          "name": "app_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          },
          "description": "App ID"
        }
      ],
      "get": {
        "operationId": "getApp",
        "summary": "Get app",
        "description": "Get details of a single app, including all channels.",
        "tags": ["Apps"],
        "responses": {
          "200": {
            "description": "App details",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AppResponse"
                }
              }
            }
          },
          "404": {
            "description": "App not found or not owned by caller",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "deleteApp",
        "summary": "Delete app",
        "description": "Delete an app. Stops all containers, removes the git repo, and cleans up DNS routes.",
        "tags": ["Apps"],
        "responses": {
          "204": {
            "description": "App deleted"
          },
          "404": {
            "description": "App not found or not owned by caller",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/apps/{app_id}/channels": {
      "parameters": [
        {
          "name": "app_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          },
          "description": "App ID"
        }
      ],
      "get": {
        "operationId": "listChannels",
        "summary": "List channels",
        "description": "List all channels for an app.",
        "tags": ["Channels"],
        "responses": {
          "200": {
            "description": "List of channels",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ChannelResponse"
                  }
                }
              }
            }
          },
          "404": {
            "description": "App not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createChannel",
        "summary": "Create channel",
        "description": "Create a new channel (e.g. a preview or staging environment). Requires channel:* scope.",
        "tags": ["Channels"],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateChannelRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Channel created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ChannelResponse"
                }
              }
            }
          },
          "400": {
            "description": "Invalid name, reserved name, or invalid git_ref_binding",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "Token lacks channel:* scope",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "App not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/apps/{app_id}/channels/{channel_id}": {
      "parameters": [
        {
          "name": "app_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          },
          "description": "App ID"
        },
        {
          "name": "channel_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          },
          "description": "Channel ID"
        }
      ],
      "get": {
        "operationId": "getChannel",
        "summary": "Get channel",
        "description": "Get details of a single channel.",
        "tags": ["Channels"],
        "responses": {
          "200": {
            "description": "Channel details",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ChannelResponse"
                }
              }
            }
          },
          "404": {
            "description": "App or channel not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "deleteChannel",
        "summary": "Delete channel",
        "description": "Delete a channel. Stops the container and removes DNS routes. Cannot delete the prod channel.",
        "tags": ["Channels"],
        "responses": {
          "204": {
            "description": "Channel deleted"
          },
          "400": {
            "description": "Cannot delete the prod channel",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "App or channel not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/apps/{app_id}/channels/{channel_id}/deploy": {
      "parameters": [
        {
          "name": "app_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          },
          "description": "App ID"
        },
        {
          "name": "channel_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          },
          "description": "Channel ID"
        }
      ],
      "post": {
        "operationId": "deploy",
        "summary": "Trigger deploy",
        "description": "Trigger a deploy. Pulls the specified SHA or branch from git, builds the container, and brings it live. Requires deploy:* scope.",
        "tags": ["Deploy"],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DeployRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Deploy queued",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DeployResponse"
                }
              }
            }
          },
          "400": {
            "description": "Invalid SHA format",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "Token lacks deploy:* scope",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "App or channel not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/apps/{app_id}/channels/{channel_id}/logs": {
      "parameters": [
        {
          "name": "app_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          },
          "description": "App ID"
        },
        {
          "name": "channel_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          },
          "description": "Channel ID"
        }
      ],
      "get": {
        "operationId": "getDeployLogs",
        "summary": "Get deploy logs",
        "description": "Retrieve the build/deploy log for a specific deploy.",
        "tags": ["Deploy"],
        "parameters": [
          {
            "name": "deploy",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Deploy ID"
          }
        ],
        "responses": {
          "200": {
            "description": "Deploy log",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "404": {
            "description": "Deploy not found or log not yet available",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/apps/{app_id}/tree": {
      "parameters": [
        {
          "name": "app_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          },
          "description": "App ID"
        }
      ],
      "get": {
        "operationId": "getAppTree",
        "summary": "List repo files",
        "description": "List all files in the repo at the given ref. Returns the resolved SHA and a flat list of blob paths with sizes. Lets a stateless agent see the current contents before editing.",
        "tags": ["Apps"],
        "parameters": [
          {
            "name": "ref",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "default": "master"
            },
            "description": "Branch name or SHA. Defaults to master."
          }
        ],
        "responses": {
          "200": {
            "description": "Repo tree",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TreeResponse"
                }
              }
            }
          },
          "404": {
            "description": "App not found, or ref does not exist (e.g. empty repo)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/apps/{app_id}/blob": {
      "parameters": [
        {
          "name": "app_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          },
          "description": "App ID"
        }
      ],
      "get": {
        "operationId": "getAppBlob",
        "summary": "Read a file",
        "description": "Return the raw bytes of a single file in the repo at the given ref. Useful when an agent needs to read existing content before modifying it.",
        "tags": ["Apps"],
        "parameters": [
          {
            "name": "ref",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "default": "master"
            },
            "description": "Branch name or SHA. Defaults to master."
          },
          {
            "name": "path",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Repository-relative path of the file."
          }
        ],
        "responses": {
          "200": {
            "description": "File contents",
            "content": {
              "application/octet-stream": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "404": {
            "description": "App, ref, or file not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/apps/{app_id}/changeset": {
      "parameters": [
        {
          "name": "app_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          },
          "description": "App ID"
        }
      ],
      "post": {
        "operationId": "commitChangeset",
        "summary": "Commit a changeset",
        "description": "Apply a sparse changeset to the repo and create one real git commit on top of ref's current HEAD (or as the initial commit on an empty branch). String values upsert files, null deletes, absent paths are unchanged. Does not deploy — call POST /apps/{id}/channels/{cid}/deploy with the returned SHA. Requires repo:* scope.",
        "tags": ["Apps"],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ChangesetRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Commit created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ChangesetResponse"
                }
              }
            }
          },
          "400": {
            "description": "Invalid path, branch name, or empty message",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "Token lacks repo:* scope",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "App not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/apps/{app_id}/env": {
      "parameters": [
        {
          "name": "app_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          },
          "description": "App ID"
        }
      ],
      "post": {
        "operationId": "setEnv",
        "summary": "Set environment variable",
        "description": "Set an environment variable for the app. Creates or updates the key. Takes effect on the next deploy. Requires deploy:* scope.",
        "tags": ["Environment"],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpsertEnvRequest"
              }
            }
          }
        },
        "responses": {
          "204": {
            "description": "Environment variable set"
          },
          "400": {
            "description": "Invalid key format or reserved key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "Token lacks deploy:* scope",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "App not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/apps/{app_id}/env/{key}": {
      "parameters": [
        {
          "name": "app_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          },
          "description": "App ID"
        },
        {
          "name": "key",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          },
          "description": "Environment variable key"
        }
      ],
      "delete": {
        "operationId": "deleteEnv",
        "summary": "Delete environment variable",
        "description": "Delete an environment variable. Takes effect on the next deploy.",
        "tags": ["Environment"],
        "responses": {
          "204": {
            "description": "Environment variable deleted"
          },
          "404": {
            "description": "App not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/tokens": {
      "post": {
        "operationId": "createToken",
        "summary": "Create token",
        "description": "Create a new API token for the authenticated user.",
        "tags": ["Tokens"],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateTokenRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Token created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreateTokenResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/tokens/{token_id}": {
      "parameters": [
        {
          "name": "token_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          },
          "description": "Token ID"
        }
      ],
      "delete": {
        "operationId": "revokeToken",
        "summary": "Revoke token",
        "description": "Revoke a token. The token is immediately invalidated.",
        "tags": ["Tokens"],
        "responses": {
          "204": {
            "description": "Token revoked"
          },
          "404": {
            "description": "Token not found or not owned by caller",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/api/user/stats": {
      "get": {
        "operationId": "getUserStats",
        "summary": "User dashboard stats",
        "description": "Get dashboard statistics for the authenticated user.",
        "tags": ["User"],
        "responses": {
          "200": {
            "description": "User statistics",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UserStatsResponse"
                }
              }
            }
          },
          "404": {
            "description": "User not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "API token prefixed with xh_"
      }
    },
    "schemas": {
      "ErrorBody": {
        "type": "object",
        "required": ["code", "message"],
        "properties": {
          "code": {
            "type": "string",
            "description": "Machine-readable error code",
            "example": "not_found"
          },
          "message": {
            "type": "string",
            "description": "Human-readable error message",
            "example": "app not found"
          }
        }
      },
      "ErrorEnvelope": {
        "type": "object",
        "required": ["error"],
        "properties": {
          "error": {
            "$ref": "#/components/schemas/ErrorBody"
          }
        }
      },
      "CreateAppRequest": {
        "type": "object",
        "required": ["name"],
        "properties": {
          "name": {
            "type": "string",
            "description": "App name. DNS label rules.",
            "example": "my-site"
          },
          "template": {
            "type": "string",
            "description": "App template: static or node",
            "enum": ["static", "app", "node"],
            "default": "static",
            "example": "static"
          }
        }
      },
      "ChannelResponse": {
        "type": "object",
        "required": ["id", "name", "hostname", "git_ref_binding", "current_sha", "status"],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "example": "7c9e6679-7425-40de-944b-e07fc1f90ae7"
          },
          "name": {
            "type": "string",
            "example": "prod"
          },
          "hostname": {
            "type": "string",
            "example": "my-site-alice.xhostd.com"
          },
          "git_ref_binding": {
            "type": "string",
            "description": "Git ref binding (branch:<name> or branch:*)",
            "example": "branch:master"
          },
          "current_sha": {
            "type": "string",
            "nullable": true,
            "description": "Currently deployed SHA, or null if never deployed",
            "example": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2"
          },
          "status": {
            "type": "string",
            "enum": ["provisioning", "running"],
            "example": "running"
          }
        }
      },
      "AppResponse": {
        "type": "object",
        "required": ["id", "name", "repo_url", "template", "created_at", "channels"],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "example": "f47ac10b-58cc-4372-a567-0e02b2c3d479"
          },
          "name": {
            "type": "string",
            "example": "my-site"
          },
          "repo_url": {
            "type": "string",
            "format": "uri",
            "example": "https://git.xhostd.com/alice/my-site.git"
          },
          "template": {
            "type": "string",
            "enum": ["static", "app"],
            "example": "static"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "example": "2026-04-22T10:30:00Z"
          },
          "channels": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ChannelResponse"
            }
          }
        }
      },
      "AppListResponse": {
        "type": "object",
        "required": ["apps"],
        "properties": {
          "apps": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AppResponse"
            }
          }
        }
      },
      "CreateChannelRequest": {
        "type": "object",
        "required": ["name", "git_ref_binding"],
        "properties": {
          "name": {
            "type": "string",
            "description": "Channel name. DNS label rules. Cannot be prod.",
            "example": "staging"
          },
          "git_ref_binding": {
            "type": "string",
            "description": "Git ref binding: branch:<name> or branch:*",
            "example": "branch:*"
          }
        }
      },
      "DeployRequest": {
        "type": "object",
        "required": ["sha"],
        "properties": {
          "sha": {
            "type": "string",
            "description": "40-char hex SHA or branch name (e.g. master, HEAD)",
            "example": "HEAD"
          }
        }
      },
      "DeployResponse": {
        "type": "object",
        "required": ["deploy_id", "channel_id", "status"],
        "properties": {
          "deploy_id": {
            "type": "string",
            "format": "uuid",
            "example": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d"
          },
          "channel_id": {
            "type": "string",
            "format": "uuid",
            "example": "7c9e6679-7425-40de-944b-e07fc1f90ae7"
          },
          "status": {
            "type": "string",
            "enum": ["queued", "running", "success", "failed"],
            "example": "queued"
          }
        }
      },
      "TreeFileEntry": {
        "type": "object",
        "required": ["path", "kind"],
        "properties": {
          "path": {
            "type": "string",
            "description": "Repository-relative file path",
            "example": "index.html"
          },
          "kind": {
            "type": "string",
            "enum": ["blob"],
            "example": "blob"
          },
          "size": {
            "type": "integer",
            "nullable": true,
            "description": "File size in bytes",
            "example": 142
          }
        }
      },
      "TreeResponse": {
        "type": "object",
        "required": ["ref", "sha", "files"],
        "properties": {
          "ref": {
            "type": "string",
            "description": "The ref that was queried",
            "example": "master"
          },
          "sha": {
            "type": "string",
            "description": "Resolved 40-char commit SHA at this ref",
            "example": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2"
          },
          "files": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/TreeFileEntry"
            }
          }
        }
      },
      "ChangesetRequest": {
        "type": "object",
        "required": ["message", "changes"],
        "properties": {
          "ref": {
            "type": "string",
            "description": "Branch to commit on top of. Created if it does not exist.",
            "default": "master",
            "example": "master"
          },
          "message": {
            "type": "string",
            "description": "Commit message",
            "example": "agent: update headline"
          },
          "changes": {
            "type": "object",
            "description": "Map of repo-relative path to new content (string upserts) or null (delete). Absent paths are unchanged.",
            "additionalProperties": {
              "type": "string",
              "nullable": true
            },
            "example": {
              "index.html": "<!doctype html><h1>hello</h1>",
              "old.css": null
            }
          }
        }
      },
      "ChangesetResponse": {
        "type": "object",
        "required": ["sha"],
        "properties": {
          "sha": {
            "type": "string",
            "description": "40-char SHA of the new commit. Pass to POST /apps/{id}/channels/{cid}/deploy to ship.",
            "example": "def456abcdef456abcdef456abcdef456abcdef4"
          }
        }
      },
      "UpsertEnvRequest": {
        "type": "object",
        "required": ["key", "value"],
        "properties": {
          "key": {
            "type": "string",
            "description": "Env var name. Must match ^[A-Z_][A-Z0-9_]*$. Cannot be XHOST_USER or XHOST_SHA.",
            "example": "DATABASE_URL"
          },
          "value": {
            "type": "string",
            "description": "Env var value. Encrypted at rest.",
            "example": "postgres://localhost/mydb"
          }
        }
      },
      "CreateTokenRequest": {
        "type": "object",
        "properties": {
          "label": {
            "type": "string",
            "nullable": true,
            "description": "Human-readable label",
            "example": "ci"
          }
        }
      },
      "CreateTokenResponse": {
        "type": "object",
        "required": ["token_id", "plaintext", "scopes", "created_at"],
        "properties": {
          "token_id": {
            "type": "string",
            "format": "uuid",
            "example": "c56a4180-65aa-42ec-a945-5fd21dec0538"
          },
          "plaintext": {
            "type": "string",
            "description": "The token value. Only returned at creation time.",
            "example": "xh_live_newtoken123"
          },
          "scopes": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": ["repo:*", "deploy:*", "channel:*"]
            },
            "example": ["repo:*", "deploy:*", "channel:*"]
          },
          "label": {
            "type": "string",
            "nullable": true,
            "example": "ci"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "example": "2026-04-22T11:00:00Z"
          }
        }
      },
      "UserStatsResponse": {
        "type": "object",
        "properties": {
          "username": { "type": "string", "example": "alice" },
          "user_id": { "type": "string", "format": "uuid" },
          "platform": {
            "type": "object",
            "properties": {
              "apps": { "type": "integer", "example": 3 },
              "channels": { "type": "integer", "example": 5 },
              "running_channels": { "type": "integer", "example": 4 },
              "deploys_last_hour": { "type": "integer", "example": 1 },
              "deploys_last_day": { "type": "integer", "example": 7 },
              "success_last_day": { "type": "integer", "example": 6 },
              "failed_last_day": { "type": "integer", "example": 1 }
            }
          },
          "resources": {
            "type": "object",
            "properties": {
              "mem_current_mb": { "type": "number", "example": 45.2 },
              "mem_limit_mb": { "type": "number", "example": 256.0 },
              "mem_percent": { "type": "number", "example": 17.7 },
              "cpu_current_percent": { "type": "number", "example": 2.5 },
              "cpu_avg_percent": { "type": "number", "example": 1.1 },
              "cpu_usage_sec": { "type": "number", "example": 142.5 }
            }
          },
          "sites": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "hostname": { "type": "string", "example": "my-site-alice.xhostd.com" },
                "repo": { "type": "string", "example": "alice/my-site" },
                "branch": { "type": "string", "example": "master" },
                "status": { "type": "string", "enum": ["provisioning", "running"] },
                "sha": { "type": "string", "example": "abc1234" },
                "latest_deploy_id": { "type": "string", "format": "uuid" },
                "latest_deploy_status": { "type": "string", "enum": ["queued", "running", "success", "failed"] }
              }
            }
          },
          "collected_at": { "type": "string", "example": "2026-04-24 10:30:00 UTC" }
        }
      }
    }
  },
  "tags": [
    {
      "name": "Apps",
      "description": "Create, list, and manage apps"
    },
    {
      "name": "Channels",
      "description": "Manage deploy channels (prod, staging, preview)"
    },
    {
      "name": "Deploy",
      "description": "Trigger deploys and read deploy logs"
    },
    {
      "name": "Environment",
      "description": "Manage app environment variables"
    },
    {
      "name": "Tokens",
      "description": "Create and revoke API tokens"
    },
    {
      "name": "User",
      "description": "User dashboard and statistics"
    }
  ]
}
