{
  "openapi": "3.0.3",
  "info": {
    "title": "xhostd API",
    "description": "Deploy websites and apps from your terminal. xhostd is a hosting platform that deploys static sites and dynamic applications via a git-based workflow.",
    "version": "1.3.0",
    "contact": {
      "name": "xhostd",
      "url": "https://docs.randomimity.com"
    }
  },
  "servers": [
    {
      "url": "https://api.randomimity.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, or neither sha nor ref given",
            "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 status and log",
        "description": "One deploy's status plus a byte window of its build log. Read the outcome from the status field, never from the log text. A queued deploy with no log yet answers 200 with an empty log and log_bytes 0.",
        "tags": [
          "Deploy"
        ],
        "parameters": [
          {
            "name": "deploy",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Deploy ID"
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 0
            },
            "description": "Byte offset to read the log from. Without it, the reply carries the last max_bytes bytes of the log, advanced past the first newline so the window starts on a whole line. An explicit offset reads byte-exactly, with no line snapping."
          },
          {
            "name": "max_bytes",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 262144,
              "default": 16384
            },
            "description": "Window size in bytes"
          }
        ],
        "responses": {
          "200": {
            "description": "Deploy status and log window",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DeployLogResponse"
                }
              }
            }
          },
          "404": {
            "description": "Deploy not found under this app and channel",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/apps/{app_id}/channels/{channel_id}/domains": {
      "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": "listChannelDomains",
        "summary": "List custom domains",
        "description": "List every custom domain attached to a channel.",
        "tags": [
          "CustomDomains"
        ],
        "responses": {
          "200": {
            "description": "Attached custom domains",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DomainListResponse"
                }
              }
            }
          },
          "404": {
            "description": "App or channel not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "addCustomDomain",
        "summary": "Attach a custom domain",
        "description": "Attach a custom hostname to the channel. Up to 5 per channel; globally unique across xhostd. Returns the DNS records the user must create at their registrar before calling /verify.",
        "tags": [
          "CustomDomains"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AddDomainRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Domain attached (status=pending)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DomainResponse"
                }
              }
            }
          },
          "400": {
            "description": "Invalid domain or domain_limit_reached",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "App or channel not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "409": {
            "description": "domain_taken — another channel owns this hostname",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/apps/{app_id}/channels/{channel_id}/domains/{domain}/verify": {
      "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"
        },
        {
          "name": "domain",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          },
          "description": "The attached custom hostname"
        }
      ],
      "post": {
        "operationId": "verifyCustomDomain",
        "summary": "Verify DNS for an attached domain",
        "description": "Re-checks the TXT token and routing record. Idempotent + retryable. Status flips pending→verified on a passing check; a verified domain only downgrades on definitive failures (txt_nxdomain, txt_token_mismatch, dns_not_pointing, domain_nxdomain).",
        "tags": [
          "CustomDomains"
        ],
        "responses": {
          "200": {
            "description": "Updated domain row",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DomainResponse"
                }
              }
            }
          },
          "404": {
            "description": "Domain not attached to this channel",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/apps/{app_id}/channels/{channel_id}/domains/{domain}": {
      "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"
        },
        {
          "name": "domain",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          },
          "description": "The attached custom hostname"
        }
      ],
      "delete": {
        "operationId": "removeCustomDomain",
        "summary": "Detach a custom domain",
        "description": "Removes the row and the Caddy route (best-effort); cert renewals stop and the cached cert expires on its own.",
        "tags": [
          "CustomDomains"
        ],
        "responses": {
          "200": {
            "description": "Detached",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Domain not attached to this channel",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/apps/{app_id}/channels/{channel_id}/postgres": {
      "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": "getChannelPostgres",
        "summary": "Get channel Postgres status",
        "description": "Inspect the channel's Postgres database: db name, role, status, live connection count, and storage usage.",
        "tags": [
          "Postgres"
        ],
        "responses": {
          "200": {
            "description": "Channel Postgres status",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ChannelPostgresStatus"
                }
              }
            }
          },
          "404": {
            "description": "App, channel, or schema row not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/apps/{app_id}/channels/{channel_id}/postgres/reset": {
      "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": "resetChannelPostgres",
        "summary": "Reset channel Postgres database",
        "description": "DESTRUCTIVE: empties the channel's database, permanently destroying all data and migration history in it. Intended for deliberate, human-approved use. The role and password are preserved, so the same DATABASE_URL keeps working. Applies to any channel including prod (no prod gate, unlike restore). Requires a typed confirmation and each reset is audit-logged.",
        "tags": [
          "Postgres"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PostgresResetRequest"
              }
            }
          }
        },
        "responses": {
          "204": {
            "description": "Database reset"
          },
          "400": {
            "description": "confirm_db_name does not match the channel's db_name",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "App, channel, or schema row not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "409": {
            "description": "Channel postgres is not in ready state",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "503": {
            "description": "Postgres admin pool is not configured (degraded mode)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/apps/{app_id}/channels/{channel_id}/postgres/dump": {
      "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": "dumpChannelPostgres",
        "summary": "Dump channel Postgres database",
        "description": "Stream a pg_dump of the channel's database. That channel's data and nothing else. Useful for backups and migrating data between channels. Each dump is audit-logged.",
        "tags": [
          "Postgres"
        ],
        "responses": {
          "200": {
            "description": "Raw pg_dump output streamed as application/sql with a Content-Disposition attachment header.",
            "content": {
              "application/sql": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "404": {
            "description": "App, channel, or schema row not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "409": {
            "description": "Channel postgres is not in ready state",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "503": {
            "description": "Postgres admin pool is not configured (degraded mode)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/me/postgres/storage": {
      "get": {
        "operationId": "getUserPostgresStorage",
        "summary": "User Postgres storage rollup",
        "description": "Report total Postgres storage and database count for the authenticated user, across all channels.",
        "tags": [
          "Postgres"
        ],
        "responses": {
          "200": {
            "description": "User-level Postgres storage rollup",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UserPostgresStorage"
                }
              }
            }
          },
          "503": {
            "description": "Postgres admin pool is not configured (degraded mode), or a database could not be measured — a partial total is never reported",
            "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). Three ways to change a file, mixed freely in one commit: changes (whole content; string upserts, null deletes), edits (anchored replacement inside an existing file), and patches (anchored hunks). Absent paths are unchanged, and a path belongs to exactly one field. Matching for edits and patches is byte-exact, and any failure fails the whole commit. 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, empty message, or an edit or hunk that did not resolve",
            "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 or secret",
        "description": "Set (upsert) an environment variable or secret. App-level by default; pass channel_id for a per-channel override. 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, or channel_id not a channel of this app",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      },
      "get": {
        "operationId": "listEnv",
        "summary": "List environment variables and secrets",
        "description": "List an app's environment variables and secrets. Without channel_id, raw rows (app-level and per-channel). With channel_id, the resolved view for that channel: app defaults merged with the channel's overrides, the override winning. Plain values are returned in cleartext; the list response returns metadata only for secrets (value is null) — read a secret's value with GET /apps/{app_id}/env/{key}/value, where each reveal is audit-logged. Requires deploy:* scope.",
        "tags": [
          "Environment"
        ],
        "parameters": [
          {
            "name": "channel_id",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Resolve the view for this channel."
          }
        ],
        "responses": {
          "200": {
            "description": "Environment list",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EnvListResponse"
                }
              }
            }
          },
          "403": {
            "description": "Token lacks deploy:* scope",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "App not found, or channel_id not a channel of this app",
            "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 or secret",
        "description": "Delete an environment variable or secret. With channel_id, deletes only that channel's override; without it, deletes the app-level default. 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"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "channel_id",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Delete only this channel's override."
          }
        ]
      }
    },
    "/apps/{app_id}/channels/{channel_id}/deploys/{deploy_id}/env": {
      "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"
        },
        {
          "name": "deploy_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          },
          "description": "Deploy ID"
        }
      ],
      "get": {
        "operationId": "getDeployEnv",
        "summary": "Get deploy env snapshot",
        "description": "Return the env snapshot recorded when a deploy started — what the app actually ran with. Secret values are masked (null); system-injected keys are listed by name only. Requires deploy:* scope.",
        "tags": [
          "Environment"
        ],
        "responses": {
          "200": {
            "description": "Deploy env snapshot",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DeployEnvSnapshotResponse"
                }
              }
            }
          },
          "403": {
            "description": "Token lacks deploy:* scope",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "Deploy not found, or it predates env snapshots",
            "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. The new token carries the full default scope set, so the calling token must carry it too: a mint never grants more scope, and never a longer life, than the caller holds. Call POST /credentials from a narrower token; it grants the scopes you hold or fewer.",
        "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"
                }
              }
            }
          },
          "403": {
            "description": "scope_denied — the calling token does not carry all nine default scopes; use POST /credentials",
            "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"
                }
              }
            }
          }
        }
      }
    },
    "/credentials": {
      "post": {
        "operationId": "createCredentials",
        "summary": "Mint unified credential",
        "description": "Mint a unified credential for the authenticated user: git password, Postgres password, object-storage and download credential, and platform API bearer in one token. A mint never grants more scope, and never a longer life, than the caller holds, so it carries every default scope the calling token holds — all nine for a general credential — unless a narrower scopes subset is requested, and lives 30 days, or until the calling token expires if that comes first, and less when a shorter expires_in is requested.",
        "tags": [
          "Tokens"
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateCredentialsRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Credential minted",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CredentialsResponse"
                }
              }
            }
          },
          "400": {
            "description": "Empty scopes list, unknown scope, or a scope the calling token does not hold; the message names the scopes you can ask for",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/registrations": {
      "post": {
        "operationId": "registerAgent",
        "summary": "Register an agent account with an SSH key",
        "description": "Open a starter account for the holder of an ssh-ed25519 key, with no person and no browser. No bearer: the SSHSIG block over the message `xhostd-register\\n<username or empty>\\n<timestamp>\\n` (namespace xhostd-register) is the proof, and the platform verifies it before any database access. The response holds a 30-day token carrying the default scopes plus email:bind, the scope that binds an address to the account, and the key is registered on the account with api_login, so it renews the token through POST /auth/ssh-key and pushes over SSH. The timestamp must sit within 300 seconds of the platform clock. Guide: https://docs.randomimity.com/guides/register-as-agent",
        "tags": [
          "Agent registration"
        ],
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RegistrationRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Account created; the token is in the body and is never shown again",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RegistrationResponse"
                }
              }
            }
          },
          "400": {
            "description": "A key line the parser refuses, a key of another type, a timestamp outside the 300-second window, a signature that does not verify, a username outside ^agent[a-z0-9]{5,35}$, or a label over 64 characters",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "409": {
            "description": "The requested username is taken, the key is registered already (sign in through POST /auth/ssh-key instead), or three random usernames in a row were taken (post again)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "422": {
            "description": "An unknown or missing field; FastAPI's validation body, not the error envelope"
          },
          "429": {
            "description": "The fleet-wide or the per-source daily registration budget is spent; retry the next day",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "503": {
            "description": "Agent registration is closed by the operator",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/auth/ssh-key": {
      "post": {
        "operationId": "loginWithSshKey",
        "summary": "Mint a token with a registered SSH key",
        "description": "Mint a fresh 30-day token, carrying the default scopes plus email:bind, for a key registered with api_login: the registration key, a key POST /ssh-keys stored with api_login true, or a key a person added in the console with the box that lets it create API tokens. This is also the one request that answers a token able to bind an address, so a caller that reads 403 from POST /me/email-verifications comes here. No bearer: the SSHSIG block over `xhostd-login\\n<fingerprint_sha256>\\n<timestamp>\\n` (namespace xhostd-login) is the proof. The fingerprint is the SHA256: value ssh-keygen -lf prints, prefix included. An account holds at most 20 tokens from this route.",
        "tags": [
          "Agent registration"
        ],
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SshKeyLoginRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Token minted",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SshKeyLoginResponse"
                }
              }
            }
          },
          "400": {
            "description": "A key line the parser refuses, a key of another type, a timestamp outside the 300-second window, or a signature that does not verify",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "No api-login key matches this fingerprint: an unknown key, or one without api_login; the two are the same answer on purpose",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "422": {
            "description": "An unknown or missing field; FastAPI's validation body, not the error envelope"
          }
        }
      }
    },
    "/ssh-keys": {
      "get": {
        "operationId": "listSshKeys",
        "summary": "List SSH keys",
        "description": "List the caller's SSH keys, newest first. Metadata only: no route returns a key.",
        "tags": [
          "SSH keys"
        ],
        "responses": {
          "200": {
            "description": "The caller's keys",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SshKeyListResponse"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "registerSshKey",
        "summary": "Register an SSH public key",
        "description": "Register one OpenSSH public-key line on the caller's account, for git over SSH. A key belongs to the account, not to one app. The platform stores the public half only and notifies the user about the new key. A key reaches every repository on the account, so the calling token must carry repo:*; api_login asks for all nine default scopes, because that is what the key goes on to mint, and for one more thing: a verified address on the account, or a calling token carrying email:bind. A token from POST /registrations or POST /auth/ssh-key carries that scope, so an agent registers a replacement key for itself. api_login true lets the key also mint a token through POST /auth/ssh-key; a registration key has it set already.",
        "tags": [
          "SSH keys"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SshKeyCreateRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Key registered",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SshKeyInfo"
                }
              }
            }
          },
          "400": {
            "description": "The line is no valid OpenSSH public key, the label is longer than 64 characters, or the request asks for api_login on a key that is not ssh-ed25519, the one type POST /auth/ssh-key accepts",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "scope_denied — the calling token does not carry repo:*, or asks for api_login without all nine default scopes",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "409": {
            "description": "The platform holds that fingerprint already; a fingerprint is unique platform-wide, so never mint a second keypair to clear this. Also the answer when the request asks for api_login while the account holds no verified address and the calling token carries no email:bind: sign the login message with an api_login key the account already holds and retry with the token POST /auth/ssh-key answers, verify an address, or register the key without api_login",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "422": {
            "description": "An unknown or missing field; FastAPI's validation body, not the error envelope"
          }
        }
      }
    },
    "/ssh-keys/{key_id}": {
      "parameters": [
        {
          "name": "key_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          },
          "description": "SSH key ID"
        }
      ],
      "delete": {
        "operationId": "deleteSshKey",
        "summary": "Delete an SSH key",
        "description": "Delete one SSH key the caller owns. The delete is the whole revoke, so a push with that key fails at once.",
        "tags": [
          "SSH keys"
        ],
        "responses": {
          "204": {
            "description": "Key deleted"
          },
          "404": {
            "description": "No such key, or the key belongs to another account",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/me/email-verifications": {
      "post": {
        "operationId": "requestEmailVerification",
        "summary": "Mail a verification code",
        "description": "Start one email challenge for the caller's account. The platform mails an 8-character code that expires in 15 minutes. The first of the two steps that move a starter account to basic. Bearer required, carrying email:bind. The address becomes the account's sign-in email, so only POST /registrations and POST /auth/ssh-key grant that scope; a credential from POST /credentials or POST /tokens reads 403.",
        "tags": [
          "Account"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EmailVerificationRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Code mailed; the response repeats no address",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EmailVerificationResponse"
                }
              }
            }
          },
          "400": {
            "description": "The address fails the shape check",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "scope_denied — the token carries no email:bind; sign the login message with the account's SSH key and call POST /auth/ssh-key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "409": {
            "description": "The account already has a verified email",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "422": {
            "description": "An unknown or missing field; FastAPI's validation body, not the error envelope"
          },
          "429": {
            "description": "A code was sent less than 60 seconds ago",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/me/email-verifications/complete": {
      "post": {
        "operationId": "completeEmailVerification",
        "summary": "Submit the verification code",
        "description": "Prove the code. Success writes the address as the account's sign-in email, moves a starter account to basic, and queues the plan apply. From then on Google sign-in with that address opens the console for this account. Bearer required, no scope: the request carries a code alone, and it binds the address the start route chose.",
        "tags": [
          "Account"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EmailVerificationCompleteRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Address verified",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EmailVerificationCompleteResponse"
                }
              }
            }
          },
          "400": {
            "description": "Wrong code; the attempt counts",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "409": {
            "description": "The address belongs to another account; the challenge clears",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "410": {
            "description": "No verification is pending, or the code expired; request a new code",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "422": {
            "description": "An unknown or missing field; FastAPI's validation body, not the error envelope"
          },
          "429": {
            "description": "Five wrong codes locked the challenge; request a new code",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/feedback": {
      "post": {
        "operationId": "submitFeedback",
        "summary": "Submit feedback",
        "description": "Submit free-text feedback to the xhostd team about platform friction. Attributed to the authenticated user. Fire-and-forget.",
        "tags": [
          "Feedback"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/FeedbackRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Feedback recorded",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FeedbackResponse"
                }
              }
            }
          },
          "400": {
            "description": "Empty message, message longer than 4000 characters, missing or blank subject (the message reads \"a feedback report needs a subject.\"), subject longer than 120 characters, or the account reached its report limit (1000 by default; an operator raises or lowers it per account, and the message names the limit that applies)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      },
      "get": {
        "operationId": "listFeedback",
        "summary": "List your feedback reports",
        "description": "List the authenticated user's feedback reports, newest first, each with its user-visible thread of team answers and your own replies. One call answers one page of the account's reports, whichever surface filed them. Internal team notes are never listed.",
        "tags": [
          "Feedback"
        ],
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            },
            "description": "How many reports to return."
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "The pagination cursor. Omit it on the first call. When the answer carries a non-null next_cursor, older reports exist: call again and pass that value as cursor. When next_cursor is null, the caller reached the end. A cursor the route cannot read answers 400."
          }
        ],
        "responses": {
          "200": {
            "description": "The reports of the authenticated user",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FeedbackListResponse"
                }
              }
            }
          },
          "400": {
            "description": "The route cannot read the cursor",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid token",
            "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. Requires stats:read. The counts and the sites rows cover every project the caller can see, shared projects included; a repo value reads owner/project. The resources block is the caller's own memory and CPU alone.",
        "tags": [
          "User"
        ],
        "responses": {
          "200": {
            "description": "User statistics",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UserStatsResponse"
                }
              }
            }
          },
          "403": {
            "description": "Token lacks stats:read scope",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "User not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/apps/{app_id}/env/{key}/value": {
      "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"
        }
      ],
      "get": {
        "operationId": "getEnvValue",
        "summary": "Reveal one env value",
        "description": "Reveal a single environment value in cleartext — the only read path for kind=secret. With channel_id, resolved semantics: the channel override wins, falling back to the app-level default. Without it, the app-level row only. Every call records an env.reveal audit event in the app journal before the value is returned. Requires deploy:* scope. A protected action: the platform answers 403 protected_action to an agent credential, until the app owner turns agent access on in the console.",
        "tags": [
          "Environment"
        ],
        "parameters": [
          {
            "name": "channel_id",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Resolve for this channel (override wins, app default as fallback)."
          }
        ],
        "responses": {
          "200": {
            "description": "The revealed value",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EnvValueResponse"
                }
              }
            }
          },
          "403": {
            "description": "Token lacks deploy:* scope (scope_denied), or the app owner has agent access off (protected_action)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "App, channel, or env key not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/apps/{app_id}/channels/{channel_id}/images": {
      "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": "getChannelImages",
        "summary": "List built images for a channel",
        "description": "Live built-image inventory for one channel, newest first, plus the per-plan image-size cap. app and docker templates build images; static returns an empty list. images is null (never an error) when the channel's host agent is unreachable.",
        "tags": [
          "Channels"
        ],
        "responses": {
          "200": {
            "description": "Channel image inventory",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ChannelImagesResponse"
                }
              }
            }
          },
          "404": {
            "description": "App or channel not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/apps/{app_id}/channels/{channel_id}/code/download": {
      "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": "downloadChannelCode",
        "summary": "Download a checkpoint's code",
        "description": "Stream a tar of the channel's code at one checkpoint. With snapshot_id the route serves the commit that snapshot recorded, so the code matches the database of the same checkpoint. Without it the route serves the channel's current commit. For an arbitrary ref use GET /apps/{app_id}/tree instead. Each download is audit-logged. The route answers 403 download_disabled while the operator keeps the leg off.",
        "tags": [
          "Channels"
        ],
        "parameters": [
          {
            "name": "snapshot_id",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "A snapshot id from GET .../postgres/snapshots. The route serves the commit that row recorded. Omit it for the channel's current commit."
          }
        ],
        "responses": {
          "200": {
            "description": "The code of that commit, streamed as application/x-tar with a Content-Disposition attachment header.",
            "content": {
              "application/x-tar": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "403": {
            "description": "download_disabled — the operator keeps this leg off",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "App or channel not found; snapshot_not_found (the id names no row of this channel); snapshot_sha_unknown (the row records no commit, or the channel never deployed); snapshot_code_gone (a force-push and a later git gc removed the commit). snapshot_sha_unknown and snapshot_code_gone are both permanent — do not repeat the call.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/apps/{app_id}/github/connect": {
      "parameters": [
        {
          "name": "app_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          },
          "description": "App ID"
        }
      ],
      "post": {
        "operationId": "connectGithub",
        "summary": "Connect a GitHub repo",
        "description": "Connect an existing GitHub repo as the app's source of truth. Generates a per-app read-only Ed25519 deploy key and records the remote; does NOT fetch (call /github/sync after adding the returned public key as a deploy key on GitHub). remote_url must be an SSH GitHub URL (git@github.com:owner/repo.git). Requires the owner role on the app. A protected action: the platform answers 403 protected_action to an agent credential, until the app owner turns agent access on in the console.",
        "tags": [
          "GitHub"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/GithubConnectRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Repo connected; add the returned public key as a read-only deploy key on GitHub",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GithubConnectResponse"
                }
              }
            }
          },
          "400": {
            "description": "remote_url is not an SSH GitHub URL",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "The app owner has agent access off (protected_action)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "App not found or caller lacks the owner role",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "409": {
            "description": "A GitHub repo is already connected to this app",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/apps/{app_id}/github": {
      "parameters": [
        {
          "name": "app_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          },
          "description": "App ID"
        }
      ],
      "get": {
        "operationId": "getGithubStatus",
        "summary": "GitHub connection status",
        "description": "Connection status and last sync outcome. Returns connected:false (all other fields null) when no repo is connected. The private deploy key is never returned.",
        "tags": [
          "GitHub"
        ],
        "responses": {
          "200": {
            "description": "Connection status",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GithubStatusResponse"
                }
              }
            }
          },
          "404": {
            "description": "App not found or caller is not a member",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "disconnectGithub",
        "summary": "Disconnect the GitHub repo",
        "description": "Disconnect the GitHub repo and discard the deploy key. The app's internal repo and deploy history are unchanged. Requires the owner role on the app. A protected action: the platform answers 403 protected_action to an agent credential, until the app owner turns agent access on in the console.",
        "tags": [
          "GitHub"
        ],
        "responses": {
          "204": {
            "description": "Disconnected"
          },
          "403": {
            "description": "The app owner has agent access off (protected_action)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "App not found, no repo connected, or caller lacks the owner role",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/apps/{app_id}/github/sync": {
      "parameters": [
        {
          "name": "app_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          },
          "description": "App ID"
        }
      ],
      "post": {
        "operationId": "syncGithub",
        "summary": "Sync from GitHub",
        "description": "Fetch the connected GitHub remote into the app's internal repo and return the updated status (including last_sync_status and per-ref results). Requires the admin role on the app.",
        "tags": [
          "GitHub"
        ],
        "responses": {
          "200": {
            "description": "Sync finished; inspect last_sync_status for the outcome",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GithubStatusResponse"
                }
              }
            }
          },
          "404": {
            "description": "App not found, no repo connected, or caller lacks the admin role",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/apps/{app_id}/channels/{channel_id}/postgres/snapshots": {
      "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": "listChannelPostgresSnapshots",
        "summary": "List DB snapshots",
        "description": "List the channel's Postgres snapshots of every kind, newest first. xhostd takes one before each deploy (kind pre_deploy) and one per day (kind nightly); restore either with POST .../postgres/restore.",
        "tags": [
          "Postgres"
        ],
        "responses": {
          "200": {
            "description": "Snapshot list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/SnapshotListItem"
                  }
                }
              }
            }
          },
          "404": {
            "description": "App, channel, or schema row not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/apps/{app_id}/channels/{channel_id}/postgres/snapshots/{snapshot_id}/download-token": {
      "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"
        },
        {
          "name": "snapshot_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          },
          "description": "A snapshot id from GET .../postgres/snapshots."
        }
      ],
      "post": {
        "operationId": "createSnapshotDownloadToken",
        "summary": "Mint a DB snapshot download token",
        "description": "Mint a short-lived snapshots:read token for one owned snapshot. The calling token must carry snapshots:read itself, because a mint never grants more scope, and never a longer life, than the caller holds. The plaintext is returned once. The token lives one hour, or until your calling token expires if that comes first. A general credential already reaches the download URL, because snapshots:read is default-granted; mint this when you want a single-purpose token instead, such as one pasted into a curl command.",
        "tags": [
          "Postgres"
        ],
        "responses": {
          "200": {
            "description": "Download token and URL",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SnapshotDownloadTokenResponse"
                }
              }
            }
          },
          "403": {
            "description": "download_disabled — the operator keeps this leg off",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "App or channel not found; snapshot_not_found (the id names no row of this channel)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/apps/{app_id}/channels/{channel_id}/postgres/snapshots/{snapshot_id}/download": {
      "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"
        },
        {
          "name": "snapshot_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          },
          "description": "A snapshot id from GET .../postgres/snapshots."
        }
      ],
      "get": {
        "operationId": "downloadChannelPostgresSnapshot",
        "summary": "Download a DB snapshot",
        "description": "Stream one snapshot's archive. Requires a bearer token carrying snapshots:read scope (mint one with POST .../download-token). The archive is a complete custom-format pg_dump, so pg_restore reads it directly. Restore it in place with POST .../postgres/restore instead if you want the channel's own database back. Each download is audit-logged. The route answers 403 download_disabled while the operator keeps the leg off.",
        "tags": [
          "Postgres"
        ],
        "responses": {
          "200": {
            "description": "The snapshot archive, streamed as application/octet-stream with a Content-Disposition attachment header.",
            "content": {
              "application/octet-stream": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "403": {
            "description": "download_disabled (the operator keeps this leg off), or scope_denied (the token lacks snapshots:read)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "App or channel not found; snapshot_not_found (the id names no row of this channel)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/apps/{app_id}/channels/{channel_id}/postgres/restore": {
      "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": "restoreChannelPostgres",
        "summary": "Restore a DB snapshot",
        "description": "Restore the channel's database from a snapshot of either kind, replacing current data. Restoring the prod channel is blocked unless the app has the env var XHOST_ALLOW_PROD_RESTORE=1. Each restore is audit-logged.",
        "tags": [
          "Postgres"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PostgresRestoreRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Restored; the channel's Postgres status is returned",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ChannelPostgresStatus"
                }
              }
            }
          },
          "400": {
            "description": "confirm_db_name does not match the channel's db_name",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "prod_restore_blocked — prod channel without XHOST_ALLOW_PROD_RESTORE=1",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "App, channel, schema row, or snapshot not found (or the snapshot file is missing)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "409": {
            "description": "Channel is busy, or the account is undergoing scheduled maintenance",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "503": {
            "description": "Postgres admin pool is not configured (degraded mode)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/me/usage": {
      "get": {
        "operationId": "getUserUsage",
        "summary": "Account database storage and egress",
        "description": "Per-account usage: account-wide DB storage alongside the plan's storage soft cap, plus the current month's measured egress. Storage is a soft cap — over-limit warns, it never blocks. Egress carries no limit field, because no plan states an egress figure: the platform measures egress, reports it, and charges nothing for it.",
        "tags": [
          "User"
        ],
        "responses": {
          "200": {
            "description": "Usage and limits",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UserUsage"
                }
              }
            }
          },
          "503": {
            "description": "Postgres admin pool is not configured (degraded mode), or a database could not be measured — a partial total is never reported",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/plans": {
      "get": {
        "operationId": "listPlans",
        "summary": "Plan tiers and their caps",
        "description": "Every plan tier and the caps it grants, lowest rank first. No row states an egress figure, because no plan limits egress. This is the one published source of a plan number: the console, the docs and the agent tools read it rather than holding a constant. The response carries no price — billing lives at Lemon Squeezy. Public: the route takes no token, reads no database, and answers every caller the same body.",
        "tags": [
          "Plans"
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "The tier table, in ascending rank order",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlanTable"
                }
              }
            }
          }
        }
      }
    },
    "/apps/{app_id}/channels/{channel_id}/blob": {
      "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": "getChannelBlob",
        "summary": "Get channel object-store status",
        "description": "Inspect the channel's object store (blob) bucket: provisioning status, usage, and the virtual S3 endpoint/bucket the app sees.",
        "tags": [
          "Blob"
        ],
        "responses": {
          "200": {
            "description": "Channel blob status",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ChannelBlobStatus"
                }
              }
            }
          },
          "404": {
            "description": "App, channel, or blob store not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "503": {
            "description": "Object storage is unavailable",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/apps/{app_id}/channels/{channel_id}/blob/credentials": {
      "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": "getChannelBlobCredentials",
        "summary": "Mint object-store credentials",
        "description": "Return the channel's S3-compatible credentials — the only payload carrying the secret key. Each call is audit-logged. Requires blob:* scope.",
        "tags": [
          "Blob"
        ],
        "responses": {
          "200": {
            "description": "S3-compatible credentials for the channel's bucket",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ChannelBlobCredentials"
                }
              }
            }
          },
          "403": {
            "description": "Token lacks blob:* scope",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "App, channel, or blob store not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "409": {
            "description": "Blob store is not in ready state",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "503": {
            "description": "Object storage is unavailable",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/apps/{app_id}/channels/{channel_id}/blob/snapshots": {
      "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": "listChannelBlobSnapshots",
        "summary": "List blob snapshots",
        "description": "List the channel's object-store snapshots of every kind, newest first. xhostd marks one before each deploy (kind pre_deploy) and one per day (kind nightly); restore either with POST .../blob/restore.",
        "tags": [
          "Blob"
        ],
        "responses": {
          "200": {
            "description": "Snapshot list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/BlobSnapshotListItem"
                  }
                }
              }
            }
          },
          "404": {
            "description": "App, channel, or blob store not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/apps/{app_id}/channels/{channel_id}/blob/snapshots/{snapshot_id}/download-token": {
      "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"
        },
        {
          "name": "snapshot_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          },
          "description": "A checkpoint id (channel_snapshots.id) whose aligned_blob flag is true."
        }
      ],
      "post": {
        "operationId": "createBlobSnapshotDownloadToken",
        "summary": "Mint a blob snapshot download token",
        "description": "Mint a short-lived blobs:read token for one owned checkpoint. The calling token must carry blobs:read itself, because a mint never grants more scope, and never a longer life, than the caller holds. The snapshot_id is the Postgres checkpoint id; the route resolves the aligned blob leg from it. The plaintext is returned once. The token lives one hour, or until your calling token expires if that comes first. A general credential already reaches the download URL, because blobs:read is default-granted; mint this when you want a single-purpose token instead.",
        "tags": [
          "Blob"
        ],
        "responses": {
          "200": {
            "description": "Download token and URL",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SnapshotDownloadTokenResponse"
                }
              }
            }
          },
          "403": {
            "description": "download_disabled — the operator keeps this leg off",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "App or channel not found; snapshot_not_found (the id names no checkpoint of this channel)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "409": {
            "description": "no_aligned_blob_snapshot — the checkpoint exists but has no aligned object-store snapshot",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/apps/{app_id}/channels/{channel_id}/blob/snapshots/{snapshot_id}/download": {
      "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"
        },
        {
          "name": "snapshot_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          },
          "description": "A checkpoint id (channel_snapshots.id) whose aligned_blob flag is true."
        }
      ],
      "get": {
        "operationId": "downloadChannelBlobSnapshot",
        "summary": "Download a blob snapshot",
        "description": "Stream the channel's objects as they were at the checkpoint's instant, as one tar. Requires a bearer token carrying blobs:read scope (mint one with POST .../download-token). The snapshot_id is the Postgres checkpoint id; xhostd resolves the aligned blob leg and reads the object versions live at that instant, so the tar matches what POST .../blob/restore would put back. Each download is audit-logged. The route answers 403 download_disabled while the operator keeps the leg off.",
        "tags": [
          "Blob"
        ],
        "responses": {
          "200": {
            "description": "The objects of that instant, streamed as one tar with a Content-Disposition attachment header.",
            "content": {
              "application/gzip": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "403": {
            "description": "download_disabled (the operator keeps this leg off), or scope_denied (the token lacks blobs:read)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "App or channel not found; snapshot_not_found (the id names no checkpoint of this channel)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "409": {
            "description": "no_aligned_blob_snapshot (the checkpoint has no aligned object-store snapshot). Or the marker is older than the age limit, so the object versions it names can already be gone — choose a newer marker. Or the object set is above the file-count or byte limit — read the files with your own S3 client instead.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "503": {
            "description": "blob_unavailable — the object store is not configured",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/apps/{app_id}/channels/{channel_id}/blob/restore": {
      "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": "restoreChannelBlob",
        "summary": "Restore a blob snapshot",
        "description": "Restore the channel's object-store prefix to a checkpoint's point-in-time, replacing current objects. snapshot_id is the checkpoint id from GET .../postgres/snapshots whose aligned_blob flag is true; the route resolves the aligned blob leg from it. Restoring the prod channel is blocked unless the app has the env var XHOST_ALLOW_PROD_RESTORE=1. Each restore is audit-logged.",
        "tags": [
          "Blob"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BlobRestoreRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Restored; the channel's blob status is returned",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ChannelBlobStatus"
                }
              }
            }
          },
          "400": {
            "description": "confirm_channel_name does not match the channel's name",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "prod_restore_blocked — prod channel without XHOST_ALLOW_PROD_RESTORE=1",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "App, channel, or blob store not found; snapshot_not_found (the checkpoint names no row of this channel, or the resolved snapshot is too old to restore)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "409": {
            "description": "channel_busy (a deploy is queued or running on the channel); no_aligned_blob_snapshot (the checkpoint has no aligned blob leg)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "503": {
            "description": "Object storage is unavailable",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/me/blob/storage": {
      "get": {
        "operationId": "getUserBlobStorage",
        "summary": "User object-store storage rollup",
        "description": "Report total object-store usage and provisioned store count for the authenticated user, across all channels.",
        "tags": [
          "Blob"
        ],
        "responses": {
          "200": {
            "description": "User-level blob storage rollup",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UserBlobStorage"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/me/apps/{app_id}/channels/{channel_id}/stats": {
      "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": "getChannelStats",
        "summary": "Channel traffic stats",
        "description": "Aggregated access statistics for one channel. Requires stats:read. Includes requests and unique visitors by hour, status-class breakdown, top pages/404s/assets, hour-of-day distribution, and country breakdown.",
        "tags": [
          "Stats"
        ],
        "parameters": [
          {
            "name": "range",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "24h",
                "7d",
                "30d"
              ],
              "default": "24h"
            },
            "description": "Aggregation window."
          }
        ],
        "responses": {
          "200": {
            "description": "Aggregated traffic stats",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TrafficStatsResponse"
                }
              }
            }
          },
          "400": {
            "description": "Invalid range",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "Token lacks stats:read scope",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "App or channel not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/me/apps/{app_id}/stats": {
      "parameters": [
        {
          "name": "app_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          },
          "description": "App ID"
        }
      ],
      "get": {
        "operationId": "getAppStats",
        "summary": "App traffic stats",
        "description": "Aggregated access statistics summed across all of an app's channels. Requires stats:read. Same shape and range semantics as the per-channel stats endpoint.",
        "tags": [
          "Stats"
        ],
        "parameters": [
          {
            "name": "range",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "24h",
                "7d",
                "30d"
              ],
              "default": "24h"
            },
            "description": "Aggregation window."
          }
        ],
        "responses": {
          "200": {
            "description": "Aggregated traffic stats",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TrafficStatsResponse"
                }
              }
            }
          },
          "400": {
            "description": "Invalid range",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "Token lacks stats:read scope",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "App not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/exports": {
      "post": {
        "operationId": "createExport",
        "summary": "Create an export",
        "description": "Queue an export (takeout) of a channel or a whole app: code, data, and generated restore scripts. One non-terminal export per user at a time. Poll GET /exports/{export_id} for progress, then mint a download token.",
        "tags": [
          "Exports"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateExportRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Export queued",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ExportResponse"
                }
              }
            }
          },
          "400": {
            "description": "Invalid scope, missing channel_id for a channel export, or nothing deployed to export",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "App or channel not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "409": {
            "description": "An export is already in progress, or the account is undergoing scheduled maintenance",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      },
      "get": {
        "operationId": "listExports",
        "summary": "List exports",
        "description": "List the authenticated user's exports, newest first.",
        "tags": [
          "Exports"
        ],
        "responses": {
          "200": {
            "description": "Export list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ExportResponse"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/exports/{export_id}": {
      "parameters": [
        {
          "name": "export_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          },
          "description": "Export ID"
        }
      ],
      "get": {
        "operationId": "getExport",
        "summary": "Get export status",
        "description": "Status and progress of one export.",
        "tags": [
          "Exports"
        ],
        "responses": {
          "200": {
            "description": "Export status",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ExportResponse"
                }
              }
            }
          },
          "404": {
            "description": "Export not found or not owned by caller",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/exports/{export_id}/download-token": {
      "parameters": [
        {
          "name": "export_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          },
          "description": "Export ID"
        }
      ],
      "post": {
        "operationId": "createExportDownloadToken",
        "summary": "Mint a download token",
        "description": "Mint a short-lived exports:read token for a ready, owned export. The calling token must carry exports:read itself, because a mint never grants more scope, and never a longer life, than the caller holds. The plaintext is returned once. Use it as the bearer on the download URLs; it expires with the export, or with your calling token if that comes first.",
        "tags": [
          "Exports"
        ],
        "responses": {
          "200": {
            "description": "Download token and URLs",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ExportDownloadTokenResponse"
                }
              }
            }
          },
          "404": {
            "description": "Export not found, not owned by caller, or not ready",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/exports/{export_id}/download": {
      "parameters": [
        {
          "name": "export_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          },
          "description": "Export ID"
        }
      ],
      "get": {
        "operationId": "downloadExport",
        "summary": "Download the export archive",
        "description": "Stream the export.tar.gz archive. Requires a bearer token carrying exports:read scope (mint one with POST /exports/{export_id}/download-token). Each download is audit-logged.",
        "tags": [
          "Exports"
        ],
        "responses": {
          "200": {
            "description": "The export archive, streamed as application/gzip with a Content-Disposition attachment header",
            "content": {
              "application/gzip": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "403": {
            "description": "Token lacks exports:read scope",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "Export not found, not owned by caller, or not ready",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/exports/{export_id}/download/blobs": {
      "parameters": [
        {
          "name": "export_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          },
          "description": "Export ID"
        }
      ],
      "get": {
        "operationId": "downloadExportBlobs",
        "summary": "Download the export's blobs",
        "description": "Live-stream blobs.tar.gz with the export's object-store contents, pinned to the versions inventoried at build time. Requires a bearer token carrying exports:read scope. Returns 409 when the object set exceeded the download threshold (blobs_included:false) — use the generated sync-blobs.sh instead. Each download is audit-logged.",
        "tags": [
          "Exports"
        ],
        "responses": {
          "200": {
            "description": "blobs.tar.gz, streamed as application/gzip",
            "content": {
              "application/gzip": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "403": {
            "description": "Token lacks exports:read scope",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "Export not found, not owned by caller, or not ready",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "409": {
            "description": "Blobs are not included in this export (over the download threshold)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/apps/{app_id}/events": {
      "parameters": [
        {
          "name": "app_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          },
          "description": "App ID"
        }
      ],
      "get": {
        "operationId": "listAppEvents",
        "summary": "List app activity events",
        "description": "Attributed audit trail of project mutations (member changes, deploys, env writes and reveals, database operations, git pushes), newest first. No secret values appear. Any member can read. Backs the list_activity MCP tool.",
        "tags": [
          "Apps"
        ],
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 50,
              "minimum": 1,
              "maximum": 100
            },
            "description": "Max events to return (clamped to 1-100)"
          },
          {
            "name": "before",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "Only events created before this instant (pagination cursor from next_before)"
          }
        ],
        "responses": {
          "200": {
            "description": "Event list",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EventListResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "App not found or caller is not a member",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "API token prefixed with xh_. Minted by OAuth, by the console, by POST /credentials, or by POST /registrations and POST /auth/ssh-key."
      }
    },
    "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"
          }
        }
      },
      "RegistrationRequest": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "public_key",
          "timestamp",
          "signature"
        ],
        "properties": {
          "public_key": {
            "type": "string",
            "description": "One OpenSSH public-key line; ssh-ed25519 only",
            "example": "ssh-ed25519 AAAAC3Nza... agent@box"
          },
          "timestamp": {
            "type": "integer",
            "description": "Unix seconds the client signed; within 300 seconds of the platform clock",
            "example": 1788000000
          },
          "signature": {
            "type": "string",
            "description": "The armored SSHSIG block ssh-keygen -Y sign wrote over the register message under the namespace xhostd-register"
          },
          "username": {
            "type": "string",
            "pattern": "^agent[a-z0-9]{5,35}$",
            "description": "A requested name; it must equal the second line of the signed message. Absent means the platform allocates agent plus 8 random characters",
            "example": "agentlisbon7"
          },
          "label": {
            "type": "string",
            "maxLength": 64,
            "description": "The key's label on the account; default registration"
          }
        }
      },
      "RegistrationResponse": {
        "type": "object",
        "required": [
          "user_id",
          "username",
          "plan",
          "token",
          "token_expires_at",
          "ssh_key_id",
          "fingerprint_sha256",
          "git_ssh_host",
          "limits",
          "next"
        ],
        "properties": {
          "user_id": {
            "type": "string",
            "format": "uuid"
          },
          "username": {
            "type": "string",
            "example": "agent7k2m9x4q"
          },
          "plan": {
            "type": "string",
            "description": "Always starter on this route",
            "example": "starter"
          },
          "token": {
            "type": "string",
            "description": "The 30-day default-scope bearer; shown once, store it with mode 0600",
            "example": "xh_..."
          },
          "token_expires_at": {
            "type": "string",
            "format": "date-time"
          },
          "ssh_key_id": {
            "type": "string",
            "format": "uuid"
          },
          "fingerprint_sha256": {
            "type": "string",
            "description": "The SHA256: fingerprint of the key, the value the login message carries",
            "example": "SHA256:abc..."
          },
          "git_ssh_host": {
            "type": "string",
            "description": "The host of the SSH git remote",
            "example": "git.randomimity.com"
          },
          "limits": {
            "$ref": "#/components/schemas/PlanTier"
          },
          "next": {
            "type": "object",
            "description": "The routes to call next: verify_email and renew_token",
            "additionalProperties": {
              "type": "string"
            },
            "example": {
              "verify_email": "POST /me/email-verifications",
              "renew_token": "POST /auth/ssh-key"
            }
          }
        }
      },
      "SshKeyLoginRequest": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "public_key",
          "timestamp",
          "signature"
        ],
        "properties": {
          "public_key": {
            "type": "string",
            "description": "One OpenSSH public-key line; ssh-ed25519 only"
          },
          "timestamp": {
            "type": "integer",
            "description": "Unix seconds the client signed; within 300 seconds of the platform clock"
          },
          "signature": {
            "type": "string",
            "description": "The armored SSHSIG block over the login message under the namespace xhostd-login"
          }
        }
      },
      "SshKeyLoginResponse": {
        "type": "object",
        "required": [
          "token",
          "token_expires_at",
          "user_id",
          "username"
        ],
        "properties": {
          "token": {
            "type": "string",
            "example": "xh_..."
          },
          "token_expires_at": {
            "type": "string",
            "format": "date-time"
          },
          "user_id": {
            "type": "string",
            "format": "uuid"
          },
          "username": {
            "type": "string"
          }
        }
      },
      "SshKeyCreateRequest": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "public_key"
        ],
        "properties": {
          "public_key": {
            "type": "string",
            "description": "One OpenSSH public-key line: ssh-ed25519, ssh-rsa, or ecdsa-sha2-nistp256/384/521",
            "example": "ssh-ed25519 AAAAC3Nza... agent@box"
          },
          "label": {
            "type": "string",
            "maxLength": 64,
            "description": "Your own name for the key"
          },
          "api_login": {
            "type": "boolean",
            "default": false,
            "description": "True lets the key mint a token through POST /auth/ssh-key; a registration key has it set already. True needs an ssh-ed25519 key, and a verified address on the account or a calling token carrying email:bind"
          }
        }
      },
      "SshKeyInfo": {
        "type": "object",
        "required": [
          "id",
          "label",
          "algo",
          "fingerprint",
          "created_at",
          "last_used_at",
          "api_login"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "label": {
            "type": "string",
            "nullable": true
          },
          "algo": {
            "type": "string",
            "example": "ssh-ed25519"
          },
          "fingerprint": {
            "type": "string",
            "example": "SHA256:abc..."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "last_used_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "Null while the key served no git command yet"
          },
          "api_login": {
            "type": "boolean",
            "description": "True for a key that can sign in through POST /auth/ssh-key"
          }
        }
      },
      "SshKeyListResponse": {
        "type": "object",
        "required": [
          "ssh_keys"
        ],
        "properties": {
          "ssh_keys": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SshKeyInfo"
            }
          }
        }
      },
      "EmailVerificationRequest": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "email"
        ],
        "properties": {
          "email": {
            "type": "string",
            "maxLength": 254,
            "description": "The address to verify; stripped and lowercased",
            "example": "owner@example.com"
          }
        }
      },
      "EmailVerificationResponse": {
        "type": "object",
        "required": [
          "status",
          "expires_at"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "sent"
            ]
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "description": "15 minutes after the request"
          }
        }
      },
      "EmailVerificationCompleteRequest": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "code"
        ],
        "properties": {
          "code": {
            "type": "string",
            "description": "The 8-character code from the mail; stripped and lowercased",
            "example": "abcd2345"
          }
        }
      },
      "EmailVerificationCompleteResponse": {
        "type": "object",
        "required": [
          "status",
          "plan",
          "apply_queued"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "verified"
            ]
          },
          "plan": {
            "type": "string",
            "description": "The plan after the call: basic for a starter account",
            "example": "basic"
          },
          "apply_queued": {
            "type": "boolean",
            "description": "True when the route queued the plan apply; false when a move holds the account and the apply runs after it"
          }
        }
      },
      "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, app, or docker",
            "enum": [
              "static",
              "app",
              "docker"
            ],
            "default": "static",
            "example": "static"
          }
        }
      },
      "ChannelResponse": {
        "type": "object",
        "required": [
          "id",
          "name",
          "hostname",
          "git_ref_binding",
          "current_sha",
          "status",
          "pending_deploy"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "example": "7c9e6679-7425-40de-944b-e07fc1f90ae7"
          },
          "name": {
            "type": "string",
            "example": "prod"
          },
          "hostname": {
            "type": "string",
            "example": "my-site-alice.randomimity.app"
          },
          "git_ref_binding": {
            "type": "string",
            "description": "Git ref binding: branch:<name>. The legacy branch:* wildcard is deprecated and rejected at create time.",
            "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",
              "failed"
            ],
            "example": "running"
          },
          "pending_deploy": {
            "type": "object",
            "nullable": true,
            "description": "The channel's newest queued or running deploy, or null when nothing is in flight. An old current_sha next to a non-null pending_deploy means the deploy has not finished yet.",
            "required": [
              "deploy_id",
              "sha",
              "status"
            ],
            "properties": {
              "deploy_id": {
                "type": "string",
                "format": "uuid",
                "example": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d"
              },
              "sha": {
                "type": "string",
                "example": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2"
              },
              "status": {
                "type": "string",
                "enum": [
                  "queued",
                  "running"
                ],
                "example": "running"
              }
            }
          }
        }
      },
      "DeployLogResponse": {
        "type": "object",
        "required": [
          "deploy_id",
          "git_sha",
          "status",
          "started_at",
          "finished_at",
          "log_bytes",
          "offset",
          "window_bytes",
          "log"
        ],
        "properties": {
          "deploy_id": {
            "type": "string",
            "format": "uuid",
            "example": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d"
          },
          "git_sha": {
            "type": "string",
            "example": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2"
          },
          "status": {
            "type": "string",
            "description": "The deploy's outcome. Read it from this field, not from the log text.",
            "enum": [
              "queued",
              "running",
              "success",
              "failed"
            ],
            "example": "success"
          },
          "started_at": {
            "type": "string",
            "format": "date-time"
          },
          "finished_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "Null while the status is queued or running"
          },
          "log_bytes": {
            "type": "integer",
            "description": "Total size of the stored log; 0 when the deploy has no log yet",
            "example": 31004
          },
          "offset": {
            "type": "integer",
            "description": "Byte offset where the returned log window starts",
            "example": 15020
          },
          "window_bytes": {
            "type": "integer",
            "description": "Byte length of the returned window before utf-8 decoding; the next page starts at offset + window_bytes",
            "example": 15984
          },
          "log": {
            "type": "string",
            "description": "The requested window of the build log"
          }
        }
      },
      "AppResponse": {
        "type": "object",
        "required": [
          "id",
          "name",
          "repo_url",
          "template",
          "created_at",
          "external_db_access_enabled",
          "external_blob_access_enabled",
          "port_forwarding_enabled",
          "port_forwarding_available",
          "agent_protected_actions_enabled",
          "agent_protected_actions_effective",
          "channels",
          "owner_username",
          "role"
        ],
        "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.randomimity.com/alice/my-site.git"
          },
          "template": {
            "type": "string",
            "enum": [
              "static",
              "app",
              "docker"
            ],
            "example": "static"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "example": "2026-04-22T10:30:00Z"
          },
          "external_db_access_enabled": {
            "type": "boolean",
            "description": "Whether external Postgres access (via the database gateway) is enabled for this app. The toggle is a protected action: an agent credential gets 403 protected_action, until the app owner turns agent access on in the console.",
            "example": false
          },
          "external_blob_access_enabled": {
            "type": "boolean",
            "description": "Whether external object-store access is enabled for this app. The toggle is a protected action: an agent credential gets 403 protected_action, until the app owner turns agent access on in the console.",
            "example": false
          },
          "port_forwarding_enabled": {
            "type": "boolean",
            "description": "Whether public raw-TCP endpoints are on for this app. The toggle is a protected action: an agent credential gets 403 protected_action, until the app owner turns agent access on in the console.",
            "example": false
          },
          "port_forwarding_available": {
            "type": "boolean",
            "description": "Whether the plan of the app owner includes public raw-TCP endpoints. The server derives this value, and no column stores it.",
            "example": true
          },
          "agent_protected_actions_enabled": {
            "type": "boolean",
            "nullable": true,
            "description": "The raw app override of the agent-access switch, or null. Null means the app inherits the account default of the app owner. The console shows three states: Inherit, On and Off.",
            "example": null
          },
          "agent_protected_actions_effective": {
            "type": "boolean",
            "description": "The value that applies now: the app override when the owner set it, and otherwise the account default of the app owner. When this value is false, each protected action refuses an agent credential with 403 protected_action.",
            "example": false
          },
          "channels": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ChannelResponse"
            }
          },
          "owner_username": {
            "type": "string",
            "description": "Username of the app's owner (may differ from the caller on shared apps).",
            "example": "alice"
          },
          "role": {
            "type": "string",
            "enum": [
              "owner",
              "admin",
              "member"
            ],
            "description": "The caller's role on this app.",
            "example": "owner"
          }
        }
      },
      "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>. One channel per branch; the legacy branch:* wildcard is deprecated and rejected at create time.",
            "example": "branch:staging"
          }
        }
      },
      "DeployRequest": {
        "type": "object",
        "description": "At least one of sha or ref must be provided. If both are given, sha wins and ref is ignored.",
        "properties": {
          "sha": {
            "type": "string",
            "description": "40-char hex SHA or branch name (e.g. master, HEAD)",
            "example": "HEAD"
          },
          "ref": {
            "type": "string",
            "description": "Branch name to resolve and deploy. Equivalent to passing a branch name as sha.",
            "example": "master"
          }
        }
      },
      "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"
        ],
        "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
            }
          },
          "edits": {
            "type": "object",
            "additionalProperties": {
              "type": "array",
              "items": {
                "$ref": "#/components/schemas/Edit"
              }
            },
            "description": "Path -> anchored replacements. The path must already exist on ref."
          },
          "patches": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            },
            "description": "Path -> hunk text. A hunk header is '@@' or '@@ anchor'; body lines begin with ' ', '-' or '+'. No line numbers and no line counts. The path must already exist."
          }
        }
      },
      "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_]*$. Reserved (rejected, system-injected per channel): XHOST_USER, XHOST_SHA, XHOST_HTTP_PORT, PORT, XHOST_FORWARD_PORT, XHOST_READY_FILE, DATABASE_URL, DATABASE_URL_READONLY, DATABASE_HOST, DATABASE_PASSWORD, S3_ENDPOINT, S3_BUCKET, S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY, S3_REGION.",
            "example": "STRIPE_SECRET_KEY"
          },
          "value": {
            "type": "string",
            "description": "Env var value. Encrypted at rest. Capped at 16 KiB of UTF-8 — well above a single-line credential and well below the size that would stop the container starting.",
            "example": "sk_live_..."
          },
          "kind": {
            "type": "string",
            "enum": [
              "env",
              "secret"
            ],
            "description": "env = plain variable; secret = list responses return metadata only (value is null). Omitted, an existing key keeps its current kind and a new key defaults to env — only an explicit kind flips a key. Read a secret's value with GET /apps/{app_id}/env/{key}/value; each reveal is audit-logged."
          },
          "channel_id": {
            "type": "string",
            "format": "uuid",
            "nullable": true,
            "description": "Omit for an app-level default; set to a channel id for a per-channel override. At deploy time the channel override wins over the app default, and system-injected keys win over both."
          }
        }
      },
      "CreateTokenRequest": {
        "type": "object",
        "properties": {
          "label": {
            "type": "string",
            "nullable": true,
            "description": "Human-readable label",
            "example": "ci"
          },
          "expires_in": {
            "type": "integer",
            "nullable": true,
            "description": "Lifetime in seconds, at most what your calling token has left. Omit to match your calling token: no expiry when it never expires, and its expiry when it does."
          }
        }
      },
      "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_newtoken123..."
          },
          "scopes": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "example": [
              "repo:*",
              "deploy:*",
              "channel:*",
              "db:*",
              "blob:*",
              "stats:read",
              "exports:read",
              "snapshots:read",
              "blobs:read"
            ]
          },
          "label": {
            "type": "string",
            "nullable": true,
            "example": "ci"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "example": "2026-04-22T11:00:00Z"
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When the token expires, or null when it does not."
          }
        }
      },
      "AddDomainRequest": {
        "type": "object",
        "required": [
          "domain"
        ],
        "properties": {
          "domain": {
            "type": "string",
            "description": "The hostname to attach. IDNA-encoded form is stored; case-folded; trailing dot stripped.",
            "example": "app.customer.com"
          }
        }
      },
      "DnsRecords": {
        "type": "object",
        "required": [
          "txt_host",
          "txt_value",
          "cname_target",
          "a_values"
        ],
        "properties": {
          "txt_host": {
            "type": "string",
            "description": "The hostname to create a TXT record at.",
            "example": "_xhost.app.customer.com"
          },
          "txt_value": {
            "type": "string",
            "description": "The TXT record value (the per-domain verification token).",
            "example": "xhost-verify-abcdef0123456789abcdef0123456789"
          },
          "cname_target": {
            "type": "string",
            "description": "Use this as the CNAME target for subdomains.",
            "example": "prod-blog-alice.randomimity.app"
          },
          "a_values": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Use one of these as the A record value for an apex/naked domain.",
            "example": [
              "198.51.100.7"
            ]
          }
        }
      },
      "DomainResponse": {
        "type": "object",
        "required": [
          "domain",
          "status",
          "reason",
          "dns_records",
          "created_at",
          "verified_at"
        ],
        "properties": {
          "domain": {
            "type": "string",
            "example": "app.customer.com"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "verified"
            ],
            "description": "Verification state. Pending means DNS records have not yet been confirmed; verified means HTTPS is serving."
          },
          "reason": {
            "type": "string",
            "nullable": true,
            "description": "Last verify result. One of: txt_nxdomain, txt_token_mismatch, txt_lookup_failed, dns_not_pointing, domain_nxdomain, dns_lookup_failed, platform_ip_unknown. Null when last verify succeeded."
          },
          "dns_records": {
            "$ref": "#/components/schemas/DnsRecords"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "verified_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When the domain first passed verification, or null if it never has."
          }
        }
      },
      "DomainListResponse": {
        "type": "object",
        "required": [
          "domains"
        ],
        "properties": {
          "domains": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/DomainResponse"
            }
          }
        }
      },
      "ChannelPostgresStatus": {
        "type": "object",
        "required": [
          "db_name",
          "role_name",
          "status",
          "last_error",
          "connection_count",
          "connection_limit",
          "password_set",
          "storage_bytes"
        ],
        "properties": {
          "db_name": {
            "type": "string",
            "description": "The name of the channel's own Postgres database, and the typed confirmation for destructive operations",
            "example": "ch_7c9e6679742540de944be07fc1f90ae7"
          },
          "role_name": {
            "type": "string",
            "description": "The Postgres role used in DATABASE_URL",
            "example": "r_7c9e6679742540de944be07fc1f90ae7"
          },
          "status": {
            "type": "string",
            "enum": [
              "provisioning",
              "ready",
              "failed"
            ],
            "example": "ready"
          },
          "last_error": {
            "type": "string",
            "nullable": true,
            "description": "Provisioner error message if status is failed",
            "example": null
          },
          "connection_count": {
            "type": "integer",
            "description": "Live connections currently held by this role",
            "example": 2
          },
          "connection_limit": {
            "type": "integer",
            "description": "Configured CONNECTION LIMIT for the write role",
            "example": 20
          },
          "password_set": {
            "type": "boolean",
            "description": "Whether the role has a stored password",
            "example": true
          },
          "storage_bytes": {
            "type": "integer",
            "description": "Bytes this channel's data occupies on disk",
            "example": 81920
          }
        }
      },
      "PostgresResetRequest": {
        "type": "object",
        "required": [
          "confirm_db_name"
        ],
        "properties": {
          "confirm_db_name": {
            "type": "string",
            "description": "Must match the channel's current db_name exactly. Acts as a typed confirmation.",
            "example": "ch_7c9e6679742540de944be07fc1f90ae7"
          }
        }
      },
      "UserPostgresStorage": {
        "type": "object",
        "required": [
          "database_size_bytes",
          "database_count"
        ],
        "properties": {
          "database_size_bytes": {
            "type": "integer",
            "description": "Bytes the account's data occupies on disk, summed over every database that holds it",
            "example": 1572864
          },
          "database_count": {
            "type": "integer",
            "description": "Number of databases that total was measured across",
            "example": 5
          }
        }
      },
      "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": 128.0
              },
              "mem_percent": {
                "type": "number",
                "example": 35.3
              },
              "cpu_current_percent": {
                "type": "number",
                "example": 2.5
              }
            }
          },
          "sites": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "hostname": {
                  "type": "string",
                  "example": "my-site-alice.randomimity.app"
                },
                "repo": {
                  "type": "string",
                  "example": "alice/my-site"
                },
                "branch": {
                  "type": "string",
                  "example": "master"
                },
                "status": {
                  "type": "string",
                  "enum": [
                    "provisioning",
                    "running",
                    "failed"
                  ]
                },
                "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"
          }
        }
      },
      "EnvListItem": {
        "type": "object",
        "required": [
          "key",
          "kind",
          "scope",
          "channel_id",
          "updated_at",
          "value"
        ],
        "properties": {
          "key": {
            "type": "string",
            "example": "MY_VAR"
          },
          "kind": {
            "type": "string",
            "enum": [
              "env",
              "secret"
            ]
          },
          "scope": {
            "type": "string",
            "enum": [
              "app",
              "channel"
            ],
            "description": "app = app-level default; channel = channel override."
          },
          "channel_id": {
            "type": "string",
            "format": "uuid",
            "nullable": true
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          },
          "value": {
            "type": "string",
            "nullable": true,
            "description": "Cleartext for kind=env; always null for secrets in list responses — read a secret's value with GET /apps/{app_id}/env/{key}/value (audit-logged)."
          }
        }
      },
      "EnvListResponse": {
        "type": "object",
        "required": [
          "env"
        ],
        "properties": {
          "env": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/EnvListItem"
            }
          }
        }
      },
      "DeployEnvSnapshotItem": {
        "type": "object",
        "required": [
          "key",
          "kind",
          "source",
          "value"
        ],
        "properties": {
          "key": {
            "type": "string"
          },
          "kind": {
            "type": "string",
            "enum": [
              "env",
              "secret"
            ]
          },
          "source": {
            "type": "string",
            "enum": [
              "app",
              "channel"
            ],
            "description": "The scope the value resolved from."
          },
          "value": {
            "type": "string",
            "nullable": true,
            "description": "Cleartext for kind=env; always null (masked) for secrets."
          }
        }
      },
      "DeployEnvSnapshotResponse": {
        "type": "object",
        "required": [
          "deploy_id",
          "env",
          "system_keys"
        ],
        "properties": {
          "deploy_id": {
            "type": "string",
            "format": "uuid"
          },
          "env": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/DeployEnvSnapshotItem"
            }
          },
          "system_keys": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "System-injected keys, by name only — their values are credentials and are not stored in the snapshot."
          }
        }
      },
      "CreateCredentialsRequest": {
        "type": "object",
        "properties": {
          "scopes": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Optional non-empty subset of the default scope set (repo:*, deploy:*, channel:*, db:*, blob:*, stats:read, exports:read, snapshots:read, blobs:read) for a least-privilege credential. Omit for the full default scopes."
          },
          "expires_in": {
            "type": "integer",
            "nullable": true,
            "description": "Lifetime in seconds, at most 2592000 (30 days) and at most what your calling token has left. Omit for the shorter of those two. Composes with scopes: {\"scopes\": [\"repo:*\"], \"expires_in\": 3600} is an hour of git access and nothing else."
          }
        }
      },
      "CredentialsResponse": {
        "type": "object",
        "required": [
          "token",
          "username",
          "expires_at",
          "scopes"
        ],
        "properties": {
          "token": {
            "type": "string",
            "description": "Unified credential: git password, Postgres password, object-storage and download credential, and platform API bearer. Lives 30 days, or until your calling token expires if that comes first, and less when expires_in asked for less.",
            "example": "xh_abc123..."
          },
          "username": {
            "type": "string",
            "example": "alice"
          },
          "expires_at": {
            "type": "string",
            "format": "date-time"
          },
          "scopes": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "FeedbackRequest": {
        "type": "object",
        "required": [
          "message",
          "subject"
        ],
        "properties": {
          "message": {
            "type": "string",
            "maxLength": 4000,
            "description": "Feedback text. Must be non-empty after trimming."
          },
          "subject": {
            "type": "string",
            "maxLength": 120,
            "description": "Required short title for the report. An omitted, empty or whitespace-only value is refused with bad_request (400)."
          },
          "app_id": {
            "type": "string",
            "format": "uuid",
            "nullable": true,
            "description": "Optional id of the app being worked on. An unknown or inaccessible id is silently dropped."
          }
        }
      },
      "FeedbackResponse": {
        "type": "object",
        "required": [
          "id",
          "status"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "status": {
            "type": "string",
            "example": "Received"
          }
        }
      },
      "FeedbackListResponse": {
        "type": "object",
        "required": [
          "reports",
          "next_cursor"
        ],
        "properties": {
          "reports": {
            "type": "array",
            "description": "The user's reports, newest first.",
            "items": {
              "$ref": "#/components/schemas/FeedbackReport"
            }
          },
          "next_cursor": {
            "type": "string",
            "nullable": true,
            "description": "The cursor of the next page, or null when the account has no older report. Pass it back as the cursor parameter. Build no cursor of your own."
          }
        }
      },
      "FeedbackReport": {
        "type": "object",
        "required": [
          "id",
          "subject",
          "title",
          "message",
          "status",
          "source",
          "app_name",
          "created_at",
          "handled_at",
          "messages"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "subject": {
            "type": "string",
            "nullable": true,
            "description": "The title the reporter wrote, or null when they wrote none."
          },
          "title": {
            "type": "string",
            "description": "The line that names the report: its subject, or the opening of its message when it has no subject. Never null and never empty, so render it without a fallback."
          },
          "message": {
            "type": "string",
            "description": "The report body the user filed."
          },
          "status": {
            "type": "string",
            "enum": [
              "Received",
              "Resolved",
              "Closed"
            ],
            "example": "Resolved",
            "description": "Received: not acted on yet. Resolved: the team did the work. Closed: the team will not act on it."
          },
          "source": {
            "type": "string",
            "enum": [
              "agent",
              "console"
            ],
            "description": "The surface the report came from."
          },
          "app_name": {
            "type": "string",
            "nullable": true,
            "description": "The app the report carries as context, or null."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "handled_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "The time of the latest status change, or null."
          },
          "messages": {
            "type": "array",
            "description": "The thread, oldest first: the team's answers and your own replies.",
            "items": {
              "$ref": "#/components/schemas/FeedbackReportMessage"
            }
          }
        }
      },
      "FeedbackReportMessage": {
        "type": "object",
        "required": [
          "author",
          "body",
          "created_at",
          "status"
        ],
        "properties": {
          "author": {
            "type": "string",
            "enum": ["you", "team"],
            "description": "Who wrote the message: you, or the xhostd team."
          },
          "body": {
            "type": "string",
            "description": "The message text."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "status": {
            "type": "string",
            "enum": [
              "Received",
              "Resolved",
              "Closed"
            ],
            "nullable": true,
            "description": "The status the report moved to, or null when the answer records no move."
          }
        }
      },
      "EnvValueResponse": {
        "type": "object",
        "required": [
          "key",
          "kind",
          "scope",
          "value"
        ],
        "properties": {
          "key": {
            "type": "string",
            "example": "STRIPE_SECRET_KEY"
          },
          "kind": {
            "type": "string",
            "enum": [
              "env",
              "secret"
            ]
          },
          "scope": {
            "type": "string",
            "enum": [
              "app",
              "channel"
            ],
            "description": "The scope the value resolved from."
          },
          "value": {
            "type": "string",
            "description": "The cleartext value.",
            "example": "sk_live_..."
          }
        }
      },
      "ChannelImage": {
        "type": "object",
        "required": [
          "tag",
          "sha",
          "size_bytes",
          "charged_size_bytes",
          "matched_base",
          "created",
          "current"
        ],
        "properties": {
          "tag": {
            "type": "string",
            "description": "Image tag on the host",
            "example": "xhost/7c9e6679-7425-40de-944b-e07fc1f90ae7:a1b2c3d"
          },
          "sha": {
            "type": "string",
            "description": "The git SHA the image was built from",
            "example": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2"
          },
          "size_bytes": {
            "type": "integer",
            "description": "Total image size",
            "example": 213909504
          },
          "charged_size_bytes": {
            "type": "integer",
            "nullable": true,
            "description": "Size counted against the plan's image cap (base-image layers excluded when matched), or null when not computed",
            "example": 41943040
          },
          "matched_base": {
            "type": "string",
            "nullable": true,
            "description": "The recognized base image whose layers are not charged, or null",
            "example": "node:22-trixie-slim"
          },
          "created": {
            "type": "integer",
            "description": "Image creation time (unix seconds)",
            "example": 1752969600
          },
          "current": {
            "type": "boolean",
            "description": "Whether this image is the channel's currently deployed SHA",
            "example": true
          }
        }
      },
      "ChannelImagesResponse": {
        "type": "object",
        "required": [
          "images",
          "image_cap_bytes"
        ],
        "properties": {
          "images": {
            "type": "array",
            "nullable": true,
            "items": {
              "$ref": "#/components/schemas/ChannelImage"
            },
            "description": "Built images for this channel, newest first. Empty for non-docker templates. Null when the channel's host agent is unreachable."
          },
          "image_cap_bytes": {
            "type": "integer",
            "description": "Per-plan cap on an image's charged size",
            "example": 536870912
          }
        }
      },
      "GithubConnectRequest": {
        "type": "object",
        "required": [
          "remote_url"
        ],
        "properties": {
          "remote_url": {
            "type": "string",
            "description": "SSH GitHub URL of the repo to mirror.",
            "example": "git@github.com:alice/my-site.git"
          }
        }
      },
      "GithubConnectResponse": {
        "type": "object",
        "required": [
          "public_key",
          "remote_url"
        ],
        "properties": {
          "public_key": {
            "type": "string",
            "description": "The generated read-only Ed25519 deploy key. Add it to the GitHub repo's deploy keys before calling /github/sync.",
            "example": "ssh-ed25519 AAAA... xhost-deploy-f47ac10b-58cc-4372-a567-0e02b2c3d479"
          },
          "remote_url": {
            "type": "string",
            "example": "git@github.com:alice/my-site.git"
          }
        }
      },
      "GithubStatusResponse": {
        "type": "object",
        "required": [
          "connected"
        ],
        "properties": {
          "connected": {
            "type": "boolean",
            "example": true
          },
          "remote_url": {
            "type": "string",
            "nullable": true,
            "example": "git@github.com:alice/my-site.git"
          },
          "public_key": {
            "type": "string",
            "nullable": true,
            "description": "The read-only deploy public key. The private key is never returned."
          },
          "connected_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "last_synced_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "last_sync_status": {
            "type": "string",
            "nullable": true,
            "description": "Outcome of the last sync (e.g. ok, error), or null if never synced."
          },
          "last_sync_error": {
            "type": "string",
            "nullable": true,
            "description": "Error message of the last failed sync, or null."
          },
          "last_sync_refs": {
            "type": "object",
            "nullable": true,
            "description": "Per-ref results of the last sync, or null."
          }
        }
      },
      "PostgresRestoreRequest": {
        "type": "object",
        "required": [
          "confirm_db_name",
          "snapshot_id"
        ],
        "properties": {
          "confirm_db_name": {
            "type": "string",
            "description": "Must match the channel's current db_name exactly. Acts as a typed confirmation.",
            "example": "ch_7c9e6679742540de944be07fc1f90ae7"
          },
          "snapshot_id": {
            "type": "string",
            "format": "uuid",
            "description": "A snapshot id from GET .../postgres/snapshots."
          }
        }
      },
      "SnapshotListItem": {
        "type": "object",
        "required": [
          "snapshot_id",
          "kind",
          "created_at"
        ],
        "properties": {
          "snapshot_id": {
            "type": "string",
            "format": "uuid"
          },
          "deploy_id": {
            "type": "string",
            "format": "uuid",
            "nullable": true,
            "description": "The deploy this snapshot was taken before. Null on a nightly snapshot, which no deploy triggers."
          },
          "kind": {
            "type": "string",
            "enum": [
              "pre_deploy",
              "nightly"
            ],
            "description": "Why the snapshot exists. xhostd keeps the newest 1 pre_deploy snapshot on basic and the newest 3 on every paid plan, with no age cap, and deletes a nightly snapshot after the plan retention window."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "git_sha": {
            "type": "string",
            "nullable": true,
            "description": "The commit this snapshot belongs to. On a pre_deploy snapshot it is the commit that ran BEFORE the deploy, because the snapshot captures the state that commit left. Null when xhostd recorded no commit for the row — a state, not a fault. Pass it as snapshot_id to GET .../code/download to fetch the matching code."
          }
        }
      },
      "UserUsage": {
        "type": "object",
        "required": [
          "plan",
          "storage_bytes",
          "storage_limit_mb",
          "bandwidth_month_bytes"
        ],
        "properties": {
          "plan": {
            "type": "string",
            "description": "The account's plan",
            "example": "basic"
          },
          "storage_bytes": {
            "type": "integer",
            "description": "Account-wide DB storage used",
            "example": 1572864
          },
          "storage_limit_mb": {
            "type": "integer",
            "description": "Plan's DB storage soft cap, in MiB",
            "example": 500
          },
          "bandwidth_month_bytes": {
            "type": "integer",
            "description": "Egress measured in the current calendar month. No plan limits egress, so no allowance rides beside this figure",
            "example": 104857600
          }
        }
      },
      "PlanTier": {
        "type": "object",
        "required": [
          "tier",
          "rank",
          "max_channels",
          "cpu_soft_cores",
          "cpu_burst",
          "mem_limit_mb",
          "storage_mb",
          "blob_storage_bytes",
          "image_size_bytes",
          "snapshot_retention_days",
          "deploy_snapshot_keep",
          "port_forwarding",
          "agent_registration_only"
        ],
        "properties": {
          "tier": {
            "type": "string",
            "enum": [
              "starter",
              "basic",
              "builder",
              "indie",
              "studio",
              "pro"
            ],
            "description": "The tier name",
            "example": "basic"
          },
          "rank": {
            "type": "integer",
            "description": "Ascending plan order, 0 for starter. Compare two tiers with it",
            "example": 1
          },
          "max_channels": {
            "type": "integer",
            "description": "Account-wide channel cap. Apps and channels draw on this one quota, because every app includes its prod channel",
            "example": 3
          },
          "cpu_soft_cores": {
            "type": "number",
            "description": "Soft CPU entitlement in cores: the fair share under contention",
            "example": 0.1
          },
          "cpu_burst": {
            "type": "integer",
            "description": "Burst multiplier. The hard slice cap is cpu_burst x cpu_soft_cores",
            "example": 2
          },
          "mem_limit_mb": {
            "type": "integer",
            "description": "Per-container memory ceiling, in MiB",
            "example": 128
          },
          "storage_mb": {
            "type": "integer",
            "description": "Account-wide database storage, in MiB. A SOFT cap: over-limit warns and blocks nothing",
            "example": 500
          },
          "blob_storage_bytes": {
            "type": "integer",
            "description": "Account-wide object storage, in bytes. ENFORCED: the S3 gateway rejects a crossing PUT with 507. -1 means unlimited",
            "example": 2147483648
          },
          "image_size_bytes": {
            "type": "integer",
            "description": "Per-image ceiling in bytes, applied to the charged size (total minus matched platform base layers)",
            "example": 536870912
          },
          "snapshot_retention_days": {
            "type": "integer",
            "description": "Age window for a channel's nightly snapshots. The newest one always survives",
            "example": 1
          },
          "deploy_snapshot_keep": {
            "type": "integer",
            "description": "Count of newest pre_deploy snapshots a channel keeps. A pre_deploy snapshot never ages out",
            "example": 1
          },
          "port_forwarding": {
            "type": "boolean",
            "description": "Whether the tier can hold a public raw-TCP endpoint",
            "example": false
          },
          "agent_registration_only": {
            "type": "boolean",
            "description": "True for a tier only the agent registration API can create an account on; the console never offers it",
            "example": false
          }
        }
      },
      "PlanTable": {
        "type": "object",
        "required": [
          "plans"
        ],
        "properties": {
          "plans": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PlanTier"
            },
            "description": "Every tier, in ascending rank order"
          }
        }
      },
      "ChannelBlobStatus": {
        "type": "object",
        "required": [
          "status",
          "last_error",
          "usage_bytes",
          "external_enabled",
          "virtual_bucket",
          "virtual_endpoint",
          "region"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "provisioning",
              "ready",
              "failed"
            ],
            "example": "ready"
          },
          "last_error": {
            "type": "string",
            "nullable": true,
            "description": "Provisioner error message if status is failed",
            "example": null
          },
          "usage_bytes": {
            "type": "integer",
            "description": "Bytes stored in the channel's bucket",
            "example": 10485760
          },
          "external_enabled": {
            "type": "boolean",
            "description": "Whether external object-store access is enabled for the app (the toggle is a protected action)",
            "example": false
          },
          "virtual_bucket": {
            "type": "string",
            "description": "The bucket name the app sees",
            "example": "my-site-alice-xhostd-com"
          },
          "virtual_endpoint": {
            "type": "string",
            "description": "The S3 endpoint the app sees",
            "example": "https://my-site-alice.s3.randomimity.app"
          },
          "region": {
            "type": "string",
            "example": "xhost"
          }
        }
      },
      "ChannelBlobCredentials": {
        "type": "object",
        "required": [
          "access_key_id",
          "secret_access_key",
          "endpoint",
          "region",
          "bucket"
        ],
        "properties": {
          "access_key_id": {
            "type": "string"
          },
          "secret_access_key": {
            "type": "string",
            "description": "The secret key. This is the only payload that carries it."
          },
          "endpoint": {
            "type": "string",
            "example": "https://my-site-alice.s3.randomimity.app"
          },
          "region": {
            "type": "string",
            "example": "xhost"
          },
          "bucket": {
            "type": "string",
            "example": "my-site-alice-xhostd-com"
          }
        }
      },
      "BlobSnapshotListItem": {
        "type": "object",
        "required": [
          "snapshot_id",
          "kind",
          "snapshot_ts",
          "created_at"
        ],
        "properties": {
          "snapshot_id": {
            "type": "string",
            "format": "uuid"
          },
          "deploy_id": {
            "type": "string",
            "format": "uuid",
            "nullable": true,
            "description": "The deploy this snapshot marks. Null on a nightly snapshot, which no deploy triggers."
          },
          "kind": {
            "type": "string",
            "enum": [
              "pre_deploy",
              "nightly"
            ],
            "description": "Why the snapshot exists. xhostd keeps the newest 1 pre_deploy snapshot on basic and the newest 3 on every paid plan, with no age cap, and deletes a nightly snapshot after the plan retention window."
          },
          "snapshot_ts": {
            "type": "string",
            "format": "date-time",
            "description": "The point-in-time a restore rolls back to"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "pg_snapshot_id": {
            "type": "string",
            "format": "uuid",
            "nullable": true,
            "description": "The Postgres snapshot of the same checkpoint. Null when the two passes did not align. The two prunes are independent, so this id can also name a Postgres row that no longer exists. Both cases mean the same thing: the two legs are not aligned. Neither is a fault."
          }
        }
      },
      "BlobRestoreRequest": {
        "type": "object",
        "required": [
          "confirm_channel_name",
          "snapshot_id"
        ],
        "properties": {
          "confirm_channel_name": {
            "type": "string",
            "description": "Must match the channel's name exactly. Acts as a typed confirmation.",
            "example": "prod"
          },
          "snapshot_id": {
            "type": "string",
            "format": "uuid",
            "description": "The checkpoint id from GET .../postgres/snapshots whose aligned_blob flag is true. The route resolves the aligned blob leg from it."
          }
        }
      },
      "UserBlobStorage": {
        "type": "object",
        "required": [
          "blob_usage_bytes",
          "store_count"
        ],
        "properties": {
          "blob_usage_bytes": {
            "type": "integer",
            "description": "Total object-store bytes across the user's channels",
            "example": 10485760
          },
          "store_count": {
            "type": "integer",
            "description": "Number of provisioned channel blob stores",
            "example": 3
          }
        }
      },
      "TrafficStatsResponse": {
        "type": "object",
        "required": [
          "range",
          "finalized_through",
          "requests_by_hour",
          "status_breakdown",
          "top_pages",
          "top_404s",
          "top_assets",
          "unique_visitors_by_hour",
          "hour_of_day_distribution",
          "country_breakdown",
          "country_totals"
        ],
        "properties": {
          "range": {
            "type": "string",
            "enum": [
              "24h",
              "7d",
              "30d"
            ]
          },
          "finalized_through": {
            "type": "string",
            "format": "date-time",
            "description": "Start of the current UTC day; buckets at or after this are still mutable (live edge)."
          },
          "requests_by_hour": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "t": {
                  "type": "string",
                  "format": "date-time"
                },
                "hits": {
                  "type": "integer"
                }
              }
            }
          },
          "status_breakdown": {
            "type": "object",
            "description": "Hits per status class",
            "properties": {
              "2xx": {
                "type": "integer"
              },
              "3xx": {
                "type": "integer"
              },
              "4xx": {
                "type": "integer"
              },
              "5xx": {
                "type": "integer"
              }
            }
          },
          "top_pages": {
            "type": "array",
            "description": "Top pages by hits (2xx)",
            "items": {
              "type": "object",
              "properties": {
                "path": {
                  "type": "string"
                },
                "hits": {
                  "type": "integer"
                }
              }
            }
          },
          "top_404s": {
            "type": "array",
            "description": "Top missing pages (4xx)",
            "items": {
              "type": "object",
              "properties": {
                "path": {
                  "type": "string"
                },
                "hits": {
                  "type": "integer"
                }
              }
            }
          },
          "top_assets": {
            "type": "array",
            "description": "Top assets by hits (2xx)",
            "items": {
              "type": "object",
              "properties": {
                "path": {
                  "type": "string"
                },
                "hits": {
                  "type": "integer"
                }
              }
            }
          },
          "unique_visitors_by_hour": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "t": {
                  "type": "string",
                  "format": "date-time"
                },
                "unique_visitors": {
                  "type": "integer"
                }
              }
            }
          },
          "hour_of_day_distribution": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "hour": {
                  "type": "integer",
                  "minimum": 0,
                  "maximum": 23
                },
                "hits": {
                  "type": "integer"
                }
              }
            }
          },
          "country_breakdown": {
            "type": "array",
            "description": "Top 10 countries, ranked by unique_visitors descending, then hits descending, then country_code. The country_code=null (Unknown) row is not ranked: it always follows the ranked countries, so the list may hold 11 entries.",
            "items": {
              "type": "object",
              "properties": {
                "country_code": {
                  "type": "string",
                  "nullable": true,
                  "description": "ISO country code, or null when unknown"
                },
                "hits": {
                  "type": "integer"
                },
                "unique_visitors": {
                  "type": "integer"
                }
              }
            }
          },
          "country_totals": {
            "type": "object",
            "description": "Totals over every country in the window, the denominator of country_breakdown.",
            "properties": {
              "hits": {
                "type": "integer"
              },
              "unique_visitors": {
                "type": "integer",
                "description": "The SUM of the per-country distinct visitor counts, not one distinct count over the window. A visitor seen in two countries counts twice."
              },
              "countries": {
                "type": "integer",
                "description": "Distinct country codes in the window, the Unknown group included."
              }
            }
          }
        }
      },
      "CreateExportRequest": {
        "type": "object",
        "required": [
          "scope",
          "app_id"
        ],
        "properties": {
          "scope": {
            "type": "string",
            "enum": [
              "channel",
              "app"
            ],
            "description": "Export one channel or the whole app."
          },
          "app_id": {
            "type": "string",
            "format": "uuid"
          },
          "channel_id": {
            "type": "string",
            "format": "uuid",
            "nullable": true,
            "description": "Required when scope is channel; ignored for app exports."
          }
        }
      },
      "ExportResponse": {
        "type": "object",
        "required": [
          "id",
          "scope",
          "app_id",
          "channel_id",
          "status",
          "detail",
          "progress_pct",
          "size_bytes",
          "error",
          "blobs_included",
          "blobs_reason",
          "blob_object_count",
          "blob_bytes_estimate",
          "created_at",
          "finished_at",
          "expires_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "scope": {
            "type": "string",
            "enum": [
              "channel",
              "app"
            ]
          },
          "app_id": {
            "type": "string",
            "format": "uuid"
          },
          "channel_id": {
            "type": "string",
            "format": "uuid",
            "nullable": true
          },
          "status": {
            "type": "string",
            "enum": [
              "queued",
              "running",
              "ready",
              "failed",
              "expired"
            ],
            "example": "ready"
          },
          "detail": {
            "type": "string",
            "nullable": true,
            "description": "Human-readable progress detail"
          },
          "progress_pct": {
            "type": "integer",
            "example": 100
          },
          "size_bytes": {
            "type": "integer",
            "nullable": true,
            "description": "Size of the built archive, or null until ready"
          },
          "error": {
            "type": "string",
            "nullable": true
          },
          "blobs_included": {
            "type": "boolean",
            "description": "Whether the object-store contents are downloadable via /download/blobs. False when the object set exceeds the download threshold — use the generated sync-blobs.sh instead."
          },
          "blobs_reason": {
            "type": "string",
            "nullable": true,
            "description": "Why blobs are not included, or null"
          },
          "blob_object_count": {
            "type": "integer",
            "nullable": true
          },
          "blob_bytes_estimate": {
            "type": "integer",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "finished_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When the export and its download token expire"
          }
        }
      },
      "ExportDownloadTokenResponse": {
        "type": "object",
        "required": [
          "token",
          "download_url",
          "blobs_download_url",
          "expires_at",
          "blobs_included",
          "blobs_reason"
        ],
        "properties": {
          "token": {
            "type": "string",
            "description": "An exports:read bearer token. Returned once; expires with the export, or with your calling token if that comes first.",
            "example": "xh_export123..."
          },
          "download_url": {
            "type": "string",
            "example": "/exports/9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d/download"
          },
          "blobs_download_url": {
            "type": "string",
            "example": "/exports/9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d/download/blobs"
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "blobs_included": {
            "type": "boolean"
          },
          "blobs_reason": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "SnapshotDownloadTokenResponse": {
        "type": "object",
        "required": [
          "token",
          "download_url",
          "expires_at"
        ],
        "properties": {
          "token": {
            "type": "string",
            "description": "A snapshots:read or blobs:read bearer token. Returned once; lives one hour, or until your calling token expires if that comes first.",
            "example": "xh_snap123..."
          },
          "download_url": {
            "type": "string",
            "example": "/apps/f47ac10b-58cc-4372-a567-0e02b2c3d479/channels/7c9e6679-7425-40de-944b-e07fc1f90ae7/postgres/snapshots/1f0e2d3c-4b5a-6978-8796-a5b4c3d2e1f0/download"
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          }
        }
      },
      "EventResponse": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "actor_username": {
            "type": "string",
            "nullable": true,
            "description": "Acting user; null for system actions"
          },
          "action": {
            "type": "string",
            "description": "Dotted action name, e.g. deploy.create, env.reveal"
          },
          "target": {
            "type": "string",
            "nullable": true
          },
          "detail": {
            "type": "object",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "id",
          "action",
          "created_at"
        ]
      },
      "EventListResponse": {
        "type": "object",
        "properties": {
          "events": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/EventResponse"
            }
          },
          "next_before": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "Pass as before to fetch the next page; null when exhausted"
          }
        },
        "required": [
          "events"
        ]
      },
      "Edit": {
        "type": "object",
        "required": [
          "old_string",
          "new_string"
        ],
        "properties": {
          "old_string": {
            "type": "string",
            "minLength": 1,
            "description": "Text to replace. Must occur exactly once unless replace_all is true."
          },
          "new_string": {
            "type": "string",
            "description": "Replacement text."
          },
          "replace_all": {
            "type": "boolean",
            "default": false,
            "description": "Replace every occurrence instead of requiring a unique one."
          }
        }
      }
    }
  },
  "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"
    },
    {
      "name": "Postgres",
      "description": "Per-channel Postgres database status, reset, dump, snapshots, and restore"
    },
    {
      "name": "Blob",
      "description": "Per-channel object storage (S3-compatible): status, credentials, snapshots, restore"
    },
    {
      "name": "GitHub",
      "description": "Mirror an existing GitHub repo as an app's source of truth"
    },
    {
      "name": "Stats",
      "description": "Aggregated traffic statistics per channel or app"
    },
    {
      "name": "Exports",
      "description": "Export (takeout) a channel or app: code, data, and restore scripts"
    },
    {
      "name": "CustomDomains",
      "description": "Per-channel custom-domain attachment, DNS verification, and removal"
    },
    {
      "name": "Feedback",
      "description": "Feedback to the xhostd team"
    },
    {
      "name": "Plans",
      "description": "The public plan tiers and the caps each one grants"
    },
    {
      "name": "Agent registration",
      "description": "Open an account and mint a token with an SSH key, with no person present"
    },
    {
      "name": "SSH keys",
      "description": "The account's SSH public keys, for git over SSH and for key sign-in"
    },
    {
      "name": "Account",
      "description": "Verify an email address; moves a starter account to basic"
    }
  ]
}
