{
  "openapi": "3.1.0",
  "info": {
    "title": "Tuqo REST API",
    "version": "2026-09-07",
    "description": "Public REST API of Tuqo, a static hosting platform, for AI agents and integrations: sites, deploys (archive, file set, or manifest with per-blob upload), custom domains, forms and auto-reply, gated access, member registration, analytics and Git CD. Authentication is a project API key: `Authorization: Bearer tqk_...`; every operation is scoped to the key's project, and resources of other projects are indistinguishable from missing ones (404). All successful responses are 200 with JSON; errors are `{\"message\": \"...\"}`. Parity with the MCP server mcp.tuqo.ru: `operationId` values match MCP tool names where the operations are the same.\n\nPlans (panel names in Russian): Free («Бесплатный»), Start («Старт»), Pro («Профи»), Business («Бизнес»), Scale («Масштаб»), Enterprise («Корпоративный»).\n\nHuman-readable docs (Russian): https://tuqo.ru/api/ · MCP server card: https://mcp.tuqo.ru/.well-known/mcp.json",
    "contact": {
      "name": "Tuqo Support",
      "url": "https://tuqo.ru/support/"
    },
    "termsOfService": "https://tuqo.ru/legal/",
    "license": {
      "name": "Tuqo Public Offer",
      "url": "https://tuqo.ru/legal/offer/"
    }
  },
  "servers": [
    {
      "url": "https://api.tuqo.ru",
      "description": "Production"
    }
  ],
  "security": [
    {
      "apiKey": []
    }
  ],
  "tags": [
    {
      "name": "whoami",
      "description": "Check the API key and its project."
    },
    {
      "name": "sites",
      "description": "Sites of the project."
    },
    {
      "name": "deploys",
      "description": "Publishing: archive, file set, status, activation and rollback, preview link."
    },
    {
      "name": "blobs",
      "description": "Manifest deploy: check for missing blobs, upload blobs one by one, assemble a deploy from blobs (for heavy media sites and the CLI)."
    },
    {
      "name": "domains",
      "description": "Custom domains with ownership verification and automatic SSL."
    },
    {
      "name": "forms",
      "description": "Form submissions from static forms (the site's `form_endpoint`)."
    },
    {
      "name": "access",
      "description": "Gated access to a site: password, email code, channel subscribers; access expiry, opens limit, login screen branding."
    },
    {
      "name": "members",
      "description": "Member registration mode: members, moderation, settings, documents."
    },
    {
      "name": "autoreply",
      "description": "Auto-reply to form submitters and lead magnet (delivery files, delivery page design)."
    },
    {
      "name": "stats",
      "description": "Server-side analytics of site traffic."
    },
    {
      "name": "git",
      "description": "Git CD: connect a repository, manual deploy, webhook."
    },
    {
      "name": "trash",
      "description": "Site trash and restore."
    }
  ],
  "paths": {
    "/api/v1/whoami": {
      "get": {
        "tags": [
          "whoami"
        ],
        "operationId": "whoami",
        "summary": "Check the key and get the project",
        "description": "Returns `project_id`, the project name and the key's scopes. Recommended first call of a session. Requires no specific scope — any valid key works.",
        "x-scope": "readonly",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Whoami"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/sites": {
      "get": {
        "tags": [
          "sites"
        ],
        "operationId": "list_sites",
        "summary": "List project sites",
        "description": "Active sites of the key's project (trash excluded). Every site includes `url` and `form_endpoint`.",
        "x-scope": "readonly",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Site"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "post": {
        "tags": [
          "sites"
        ],
        "operationId": "create_site",
        "summary": "Create a site",
        "description": "Creates a site on a platform subdomain. Checks: name is not empty; subdomain is valid (400), not reserved (409), not taken (409); short subdomains (5–8 characters) require a paid plan (400); plan site quota (402/409); project is not suspended (402).",
        "x-scope": "editor",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateSiteInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The created site (code 200, not 201).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Site"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/UnsupportedMediaType"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/trashed-sites": {
      "get": {
        "tags": [
          "trash"
        ],
        "operationId": "list_trashed",
        "summary": "List trashed sites",
        "description": "Project sites pending deletion (`deletion_scheduled_at` is set). They can be restored via POST /api/v1/sites/{id}/restore.",
        "x-scope": "readonly",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Site"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/sites/{id}": {
      "get": {
        "tags": [
          "sites"
        ],
        "operationId": "get_site",
        "summary": "Get a site",
        "description": "Site by id. If the site's `checks_enabled` switch is on and the latest publish has findings, `recommendations` (site checks) is added.",
        "x-scope": "readonly",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Site id (UUID). The site must belong to the key's project, otherwise 404."
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Site"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "patch": {
        "tags": [
          "sites"
        ],
        "operationId": "update_site",
        "summary": "Rename a site or change its description",
        "description": "Updates `name` (required) and, if provided, `description`.",
        "x-scope": "editor",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Site id (UUID). The site must belong to the key's project, otherwise 404."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateSiteInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Site"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/UnsupportedMediaType"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "delete": {
        "tags": [
          "sites"
        ],
        "operationId": "delete_site",
        "summary": "Delete a site (move to trash)",
        "description": "DEFERRED deletion: the site goes to trash for 24 hours, serving stops, artifacts are kept (restore via POST /api/v1/sites/{id}/restore). Requires the `full` scope.",
        "x-scope": "full",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Site id (UUID). The site must belong to the key's project, otherwise 404."
          }
        ],
        "responses": {
          "200": {
            "description": "The site with `deletion_scheduled_at` set.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Site"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/sites/{id}/restore": {
      "post": {
        "tags": [
          "trash"
        ],
        "operationId": "restore_site",
        "summary": "Restore a site from trash",
        "description": "Restores the site. Honors the plan's site and storage quotas (402/409) and project suspension.",
        "x-scope": "editor",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Site id (UUID). The site must belong to the key's project, otherwise 404."
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Site"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/sites/{id}/enabled": {
      "post": {
        "tags": [
          "sites"
        ],
        "operationId": "set_site_enabled",
        "summary": "Enable or disable site serving",
        "description": "`enabled:false` — the site responds with a “temporarily unavailable” page. A site of a project suspended for non-payment cannot be enabled (402).",
        "x-scope": "editor",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Site id (UUID). The site must belong to the key's project, otherwise 404."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SetEnabledInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Site"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/UnsupportedMediaType"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/sites/{id}/deploys": {
      "get": {
        "tags": [
          "deploys"
        ],
        "operationId": "list_deploys",
        "summary": "List site deploys",
        "description": "All deploys of the site, newest first.",
        "x-scope": "readonly",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Site id (UUID). The site must belong to the key's project, otherwise 404."
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Deploy"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "post": {
        "tags": [
          "deploys"
        ],
        "operationId": "deploy_site",
        "summary": "Deploy from a source archive",
        "description": "The request body is the RAW bytes of a tar.gz or zip archive (not base64, not JSON), up to 50 MB. An empty body, an unrecognized archive or an oversized body → 400. If the root contains `package.json`, the build-runner builds the project on Node 20 (`npm ci && npm run build`, artifact from dist/build/out); otherwise the static files are published as is. The deploy is queued and returned with status `queued`; poll GET /api/v1/deploys/{id}. Gates: storage quota (409), deploy rate and build minutes (429), suspension (402).",
        "x-scope": "editor",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Site id (UUID). The site must belong to the key's project, otherwise 404."
          },
          {
            "name": "activate",
            "in": "query",
            "required": false,
            "schema": {
              "type": "boolean",
              "default": true
            },
            "description": "`false` — build without publishing (preview; requires a plan with preview links, otherwise 402)."
          }
        ],
        "requestBody": {
          "required": true,
          "description": "A tar.gz or zip archive (raw bytes).",
          "content": {
            "application/gzip": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            },
            "application/zip": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            },
            "application/octet-stream": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The deploy is queued (status=queued). Code 200, not 202.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Deploy"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/sites/{id}/deploy-files": {
      "post": {
        "tags": [
          "deploys"
        ],
        "operationId": "deploy_files",
        "summary": "Deploy a set of files (instant static publish)",
        "description": "Recommended path for generated static files: files are passed as is (text as `content`, binary with `encoding:\"base64\"`, unchanged files as a `{path, sha256}` reference to a project blob from GET /sites/{id}/manifest). The server computes sha256, stores missing blobs in S3 (deduplicated), writes a manifest and queues the deploy for publishing by copy (no build, no build minutes spent). Requirements: `index.html` at the root, no `package.json`, at most 2000 files, always the FULL file set. Request body up to 50 MB. One such request per server is processed at a time (semaphore), so under load the response may wait. Gates: storage quota (409), deploy rate / build minutes (429), suspension (402), preview links when `activate:false` (402). The response contains the deploy (`queued`) and, when checks are enabled, `recommendations`.",
        "x-scope": "editor",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Site id (UUID). The site must belong to the key's project, otherwise 404."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DeployFilesInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The deploy is queued (status=queued); may include `recommendations`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Deploy"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/UnsupportedMediaType"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/sites/{id}/deploys/check": {
      "post": {
        "tags": [
          "blobs"
        ],
        "operationId": "check_manifest",
        "summary": "Check which manifest blobs are missing",
        "description": "Step 1 of a manifest deploy. Accepts a list of `{path, sha256[, size]}` and returns the hashes the project does not have yet. Validation: non-empty list, ≤2000 entries, each sha256 is 64 hex characters (400). With `activate:false` the preview-link entitlement is also checked (402).",
        "x-scope": "editor",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Site id (UUID). The site must belong to the key's project, otherwise 404."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ManifestInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CheckResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/UnsupportedMediaType"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/blobs/{sha256}": {
      "put": {
        "tags": [
          "blobs"
        ],
        "operationId": "put_blob",
        "summary": "Upload a single blob",
        "description": "Step 2 of a manifest deploy. The body is the RAW bytes of the file (up to 50 MB, not empty). The sha256 of the body MUST match the hash in the path (otherwise 400). The blob is stored in the scope of the key's project (S3 + `deploy_blobs`); text blobs are additionally gzip-compressed. Gate: storage quota (409). Idempotent: repeating a PUT of the same hash is safe.",
        "x-scope": "editor",
        "parameters": [
          {
            "name": "sha256",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[0-9a-fA-F]{64}$"
            },
            "description": "sha256 of the content in hex (64 characters)."
          }
        ],
        "requestBody": {
          "required": true,
          "description": "File bytes.",
          "content": {
            "application/octet-stream": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BlobPutResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/sites/{id}/deploys/manifest": {
      "post": {
        "tags": [
          "blobs"
        ],
        "operationId": "deploy_manifest",
        "summary": "Assemble a deploy from uploaded blobs",
        "description": "Step 3 of a manifest deploy. All blobs must be uploaded beforehand — otherwise 400 in the special `{error, missing}` format (see ManifestMissingError). The static policy is checked by paths (index.html at the root, no package.json — 400). The control-plane only writes the manifest and queues a deploy with `source_kind=manifest`; the build-runner publishes it by copying inside S3. The overall ceiling is the plan's storage quota (409). Gates: rate / build minutes (429), suspension (402), preview links when `activate:false` (402).",
        "x-scope": "editor",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Site id (UUID). The site must belong to the key's project, otherwise 404."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ManifestInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The deploy is queued (status=queued); may include `recommendations`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Deploy"
                }
              }
            }
          },
          "400": {
            "description": "Invalid data (`{message}`) OR not all blobs are uploaded (`{error, missing}`).",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "$ref": "#/components/schemas/ManifestMissingError"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/UnsupportedMediaType"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/sites/{id}/manifest": {
      "get": {
        "tags": [
          "deploys"
        ],
        "operationId": "get_manifest",
        "summary": "Get the active deploy manifest",
        "description": "Files of the live version as `{path, sha256}` — for editing a site without re-uploading media: pass unchanged files to deploy-files as a `{path, sha256}` reference. Empty array if there is no active deploy or manifest (for example, the site was built from an archive or git).",
        "x-scope": "readonly",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Site id (UUID). The site must belong to the key's project, otherwise 404."
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ManifestResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/deploys/{id}": {
      "get": {
        "tags": [
          "deploys"
        ],
        "operationId": "get_deploy_status",
        "summary": "Get deploy status",
        "description": "Deploy by id: status, error, `build_mode`, `skipped_paths`, etc.",
        "x-scope": "readonly",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Deploy id (UUID). The deploy must belong to a site of the key's project, otherwise 404."
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Deploy"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "delete": {
        "tags": [
          "deploys"
        ],
        "operationId": "delete_deploy",
        "summary": "Delete deploy artifacts",
        "description": "Purges the version's artifacts from S3 (the row stays in history with `artifacts_purged=true`). The live (`active`) version cannot be deleted — 409; a deploy in `queued`/`building` — 409 (wait for it to finish).",
        "x-scope": "editor",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Deploy id (UUID). The deploy must belong to a site of the key's project, otherwise 404."
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OkResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/api/v1/deploys/{id}/activate": {
      "post": {
        "tags": [
          "deploys"
        ],
        "operationId": "activate_deploy",
        "summary": "Activate a deploy (or roll back)",
        "description": "Makes the given deploy live; a rollback to a previous version is done the same way. Allowed statuses: `ready`, `active`, `superseded`; otherwise 409. A version with purged artifacts (`artifacts_purged`) — 409. Activation clears `is_preview` and revokes the preview link; afterwards the deploy retention policy is applied. No request body.",
        "x-scope": "editor",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Deploy id (UUID). The deploy must belong to a site of the key's project, otherwise 404."
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ActivateResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/deploys/{id}/preview": {
      "post": {
        "tags": [
          "deploys"
        ],
        "operationId": "get_preview_url",
        "summary": "Get a preview link for a deploy",
        "description": "Issues (idempotently — a repeat call returns the same one) a secret link `https://preview.tuqo.ru/<token>/` to an unpublished (`ready`) or previous (`superseded`) deploy that still has artifacts. Opens without a password even on a gated site. Not issued for the live (`active`) version — 409; nor for a deploy without files — 409. Requires the Start plan or higher (402).",
        "x-scope": "editor",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Deploy id (UUID). The deploy must belong to a site of the key's project, otherwise 404."
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PreviewResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "delete": {
        "tags": [
          "deploys"
        ],
        "operationId": "revoke_preview_url",
        "summary": "Revoke a preview link",
        "description": "Revokes the link; the deploy stays unpublished.",
        "x-scope": "editor",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Deploy id (UUID). The deploy must belong to a site of the key's project, otherwise 404."
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OkResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/sites/{id}/domains": {
      "get": {
        "tags": [
          "domains"
        ],
        "operationId": "list_domains",
        "summary": "List site domains",
        "description": "Custom domains of the site.",
        "x-scope": "readonly",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Site id (UUID). The site must belong to the key's project, otherwise 404."
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Domain"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "post": {
        "tags": [
          "domains"
        ],
        "operationId": "set_custom_domain",
        "summary": "Add a custom domain",
        "description": "Adds a domain to the site and returns the TXT record for ownership verification (`_tuqo-verify.<domain>` = token). Domain already attached (to any site) — 409; plan domain quota / suspension — 409/402. After creating the TXT record (and an A record pointing to the serving IPs), call POST /api/v1/domains/{id}/verify. SSL is issued automatically after verification.",
        "x-scope": "editor",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Site id (UUID). The site must belong to the key's project, otherwise 404."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AddDomainInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DomainAddResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/UnsupportedMediaType"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/domains/{id}/verify": {
      "post": {
        "tags": [
          "domains"
        ],
        "operationId": "verify_domain",
        "summary": "Verify a domain",
        "description": "Checks the TXT record and, if it matches, marks the domain verified. The response always includes A record diagnostics in `a_record` (does not affect `verified`). External DNS queries are throttled to once per 10 s per domain — for an unverified domain a repeated call sooner returns 429; for an already verified domain the call re-checks the A record (`a_record: null` while throttled). No request body.",
        "x-scope": "editor",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Domain id (UUID). The domain must belong to a site of the key's project, otherwise 404."
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DomainVerifyResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/domains/{id}": {
      "delete": {
        "tags": [
          "domains"
        ],
        "operationId": "delete_domain",
        "summary": "Remove a custom domain",
        "description": "Deletes the site's domain. Requires the `full` scope.",
        "x-scope": "full",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Domain id (UUID). The domain must belong to a site of the key's project, otherwise 404."
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OkResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/forms/overview": {
      "get": {
        "tags": [
          "forms"
        ],
        "operationId": "get_forms_overview",
        "summary": "Get the project forms overview",
        "description": "Monthly submission quota and usage, purchased balance, number of submissions hidden over the limit, project sites with form settings and per-status counters.",
        "x-scope": "readonly",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FormsOverview"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/forms/submissions": {
      "get": {
        "tags": [
          "forms"
        ],
        "operationId": "get_form_submissions",
        "summary": "List project form submissions",
        "description": "Project submissions, newest first. Submissions hidden over the limit (`hidden=true`) have an empty `data` field — the content is revealed after purchasing capacity. Search (`q`) does not look into hidden submissions.",
        "x-scope": "readonly",
        "parameters": [
          {
            "name": "site",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Filter by site. A site of another project returns nothing."
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "verified",
                "spam",
                "archive"
              ],
              "default": "verified"
            },
            "description": "Any other value is treated as verified."
          },
          {
            "name": "q",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Search in submission content (ILIKE, substring)."
          },
          {
            "name": "field",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Restrict the search to one form field."
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 50,
              "minimum": 1,
              "maximum": 200
            },
            "description": "Clamped to 1–200."
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 0,
              "minimum": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SubmissionsResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/sites/{id}/autoreply": {
      "get": {
        "tags": [
          "autoreply"
        ],
        "operationId": "get_autoreply",
        "summary": "Get auto-reply settings",
        "description": "Settings of the email sent to the submitter, delivery files, plan entitlement and the number of emails sent this month.",
        "x-scope": "readonly",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Site id (UUID). The site must belong to the key's project, otherwise 404."
          }
        ],
        "responses": {
          "200": {
            "description": "Current auto-reply settings, delivery files and the monthly counter.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AutoreplyView"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "put": {
        "tags": [
          "autoreply"
        ],
        "operationId": "set_autoreply",
        "summary": "Save auto-reply settings",
        "description": "Full write of `enabled/subject/body/email_field` (not a patch: missing subject/body become empty). Limits: subject ≤120, body ≤4096, email_field ≤64 characters (400). Enabling (`enabled:true`) requires the Start plan or higher (402).",
        "x-scope": "editor",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Site id (UUID). The site must belong to the key's project, otherwise 404."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AutoreplyPut"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Current auto-reply settings, delivery files and the monthly counter.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AutoreplyView"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/UnsupportedMediaType"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/sites/{id}/autoreply/files": {
      "post": {
        "tags": [
          "autoreply"
        ],
        "operationId": "add_autoreply_file",
        "summary": "Add a delivery file (lead magnet)",
        "description": "The body is the RAW bytes of the file (up to 50 MB; route body limit 52 MB). Allowed types are detected by content: PDF, PNG/JPEG/GIF/WebP, zip, UTF-8 text (HTML/SVG/XML are rejected). At most three files per site (400). Requires a plan with auto-reply (402) and free storage quota (409).",
        "x-scope": "editor",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Site id (UUID). The site must belong to the key's project, otherwise 404."
          },
          {
            "name": "name",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "maxLength": 120
            },
            "description": "File name for download (truncated to 120 characters)."
          },
          {
            "name": "label",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "maxLength": 80
            },
            "description": "Label shown on the delivery page."
          }
        ],
        "requestBody": {
          "required": true,
          "description": "File bytes.",
          "content": {
            "application/octet-stream": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Current auto-reply settings, delivery files and the monthly counter.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AutoreplyView"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/api/v1/sites/{id}/autoreply/files/{fid}": {
      "delete": {
        "tags": [
          "autoreply"
        ],
        "operationId": "remove_autoreply_file",
        "summary": "Delete a delivery file",
        "description": "Removes the file from the delivery page and from storage. Unknown file — 404.",
        "x-scope": "editor",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Site id (UUID). The site must belong to the key's project, otherwise 404."
          },
          {
            "name": "fid",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Delivery file id."
          }
        ],
        "responses": {
          "200": {
            "description": "Current auto-reply settings, delivery files and the monthly counter.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AutoreplyView"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/sites/{id}/autoreply/files/{fid}/label": {
      "put": {
        "tags": [
          "autoreply"
        ],
        "operationId": "set_autoreply_file_label",
        "summary": "Set a delivery file label",
        "description": "Changes the file's label on the delivery page. Body `{label}`; a missing field = empty label.",
        "x-scope": "editor",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Site id (UUID). The site must belong to the key's project, otherwise 404."
          },
          {
            "name": "fid",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Delivery file id."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/FileLabelInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Current auto-reply settings, delivery files and the monthly counter.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AutoreplyView"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/UnsupportedMediaType"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/sites/{id}/autoreply/delivery": {
      "put": {
        "tags": [
          "autoreply"
        ],
        "operationId": "set_autoreply_delivery",
        "summary": "Set delivery page design and link lifetime",
        "description": "`link_ttl_days` — lifetime of the file link; `page_title` + `upsell_text` — page design (both or neither; ≤80/≤500 characters); `page_logo_framed`/`page_logo_rounded` — logo appearance. Texts cannot be cleared while a logo is uploaded (400).",
        "x-scope": "editor",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Site id (UUID). The site must belong to the key's project, otherwise 404."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DeliveryPut"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Current auto-reply settings, delivery files and the monthly counter.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AutoreplyView"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/UnsupportedMediaType"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/sites/{id}/autoreply/page-logo": {
      "put": {
        "tags": [
          "autoreply"
        ],
        "operationId": "set_autoreply_page_logo",
        "summary": "Upload a delivery page logo",
        "description": "The body is the raw bytes of a PNG/JPEG/WebP image up to 1 MB (SVG is rejected); the server fits it into 512×256 and compresses it. Requires the page design to be filled in (page_title + upsell_text, otherwise 400) and a plan with auto-reply (402). Route body limit — 2 MB.",
        "x-scope": "editor",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Site id (UUID). The site must belong to the key's project, otherwise 404."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "image/png": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            },
            "image/jpeg": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            },
            "image/webp": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            },
            "application/octet-stream": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Current auto-reply settings, delivery files and the monthly counter.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AutoreplyView"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "delete": {
        "tags": [
          "autoreply"
        ],
        "operationId": "remove_autoreply_page_logo",
        "summary": "Remove the delivery page logo",
        "description": "Deletes the logo.",
        "x-scope": "editor",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Site id (UUID). The site must belong to the key's project, otherwise 404."
          }
        ],
        "responses": {
          "200": {
            "description": "Current auto-reply settings, delivery files and the monthly counter.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AutoreplyView"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/sites/{id}/access": {
      "get": {
        "tags": [
          "access"
        ],
        "operationId": "get_site_access",
        "summary": "Get site access state",
        "description": "Gated access mode and parameters, login screen branding, gate events for the last 30 days and the channels of the subscribers mode.",
        "x-scope": "readonly",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Site id (UUID). The site must belong to the key's project, otherwise 404."
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AccessGetResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "put": {
        "tags": [
          "access"
        ],
        "operationId": "set_site_password",
        "summary": "Set or change the site password",
        "description": "Enables the `password` gate or changes the password. The password is generated at the given `level` (easy/medium/strong, default medium) or taken from `password` (6–128 characters). The plaintext password is returned in the response (later via GET …/access/password). Changing the password signs out everyone who has logged in. The previous gate mode (email code, etc.) is replaced. Requires the Start plan or higher (402).",
        "x-scope": "editor",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Site id (UUID). The site must belong to the key's project, otherwise 404."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SetPasswordInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SetPasswordResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/UnsupportedMediaType"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "delete": {
        "tags": [
          "access"
        ],
        "operationId": "open_site_access",
        "summary": "Open site access",
        "description": "The site is open to everyone again; issued passes are revoked. Login screen branding is kept.",
        "x-scope": "editor",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Site id (UUID). The site must belong to the key's project, otherwise 404."
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OkNextResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/sites/{id}/access/password": {
      "get": {
        "tags": [
          "access"
        ],
        "operationId": "reveal_site_password",
        "summary": "Reveal the current password",
        "description": "The only GET that returns a secret — the response has `Cache-Control: no-store`. Requires the `editor` scope (not readonly). `password: null` if the password was set before reversible storage was introduced.",
        "x-scope": "editor",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Site id (UUID). The site must belong to the key's project, otherwise 404."
          }
        ],
        "responses": {
          "200": {
            "description": "The password.",
            "headers": {
              "Cache-Control": {
                "schema": {
                  "type": "string",
                  "const": "no-store"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RevealPasswordResponse"
                },
                "example": {
                  "password": "kolibri-72-sever"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/sites/{id}/access/brand": {
      "patch": {
        "tags": [
          "access"
        ],
        "operationId": "set_site_access_brand",
        "summary": "Set login screen branding",
        "description": "Patch: an absent field is unchanged, `null` clears it. Logo — base64 PNG/JPEG/WebP up to 1 MB, the server fits it into 512×256 (400 on invalid format). The favicon cannot be configured via REST (panel/MCP only).",
        "x-scope": "editor",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Site id (UUID). The site must belong to the key's project, otherwise 404."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BrandInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BrandResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/UnsupportedMediaType"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/sites/{id}/access/expiry": {
      "patch": {
        "tags": [
          "access"
        ],
        "operationId": "set_site_access_expiry",
        "summary": "Set access expiry",
        "description": "Three mutually exclusive ways: `days` (preset 1/3/7/30 — extends from max(now, current expiry)), `until` (date or date-time in the project's time zone, at most 366 days ahead, not in the past), `clear:true` (remove the expiry; available on any plan). Setting an expiry requires the Start plan or higher (402). The login screen shows visitors the remaining time.",
        "x-scope": "editor",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Site id (UUID). The site must belong to the key's project, otherwise 404."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ExpiryInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AccessNextResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/UnsupportedMediaType"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/sites/{id}/access/opens": {
      "patch": {
        "tags": [
          "access"
        ],
        "operationId": "set_site_access_opens",
        "summary": "Set the opens limit",
        "description": "`max_opens` 3–1000 (counts devices / issued passes, not page views) or `null` — remove the limit (free). `reset_counter:true` resets the counter. Setting or resetting requires the Start plan or higher (402).",
        "x-scope": "editor",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Site id (UUID). The site must belong to the key's project, otherwise 404."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/OpensInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AccessNextResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/UnsupportedMediaType"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/sites/{id}/access/email-mode": {
      "post": {
        "tags": [
          "access"
        ],
        "operationId": "set_site_email_access",
        "summary": "Switch to email code mode",
        "description": "Switches the site to login by a code sent by email (the password is cleared). While the address list is empty nobody can log in — add addresses via POST …/access/emails. Requires the Pro plan or higher, or the paid option on the site (402). No request body.",
        "x-scope": "editor",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Site id (UUID). The site must belong to the key's project, otherwise 404."
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AccessNextResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/sites/{id}/access/subscribers-mode": {
      "post": {
        "tags": [
          "access"
        ],
        "operationId": "set_site_subscribers_access",
        "summary": "Switch to channel subscribers mode",
        "description": "Opens the site only to subscribers of the selected Telegram/MAX channels (1–3, same platform, verified, owned by the project owner — otherwise 400). Channels are connected in the owner's profile in the panel. Requires the Pro plan or higher, or the paid option (402).",
        "x-scope": "editor",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Site id (UUID). The site must belong to the key's project, otherwise 404."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SubscribersInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AccessNextResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/UnsupportedMediaType"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/sites/{id}/access/emails": {
      "get": {
        "tags": [
          "access"
        ],
        "operationId": "list_site_access_emails",
        "summary": "List access emails and login log",
        "description": "Addresses of the email code mode with device counts and last login, the list ceiling and recent login attempts from unknown addresses.",
        "x-scope": "readonly",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Site id (UUID). The site must belong to the key's project, otherwise 404."
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EmailListResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "post": {
        "tags": [
          "access"
        ],
        "operationId": "add_site_access_email",
        "summary": "Add an access email",
        "description": "Adds one address (normalized; invalid — 400). List ceiling of 1000 addresses — 409. Requires the Pro plan or higher, or the paid option (402).",
        "x-scope": "editor",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Site id (UUID). The site must belong to the key's project, otherwise 404."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AddEmailInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AccessEmail"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/UnsupportedMediaType"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/sites/{id}/access/emails/bulk": {
      "post": {
        "tags": [
          "access"
        ],
        "operationId": "add_site_access_emails",
        "summary": "Add access emails in bulk",
        "description": "Parses `raw` (separators: newline, comma, semicolon, spaces), adds new addresses and returns a breakdown: added / duplicates / invalid / overflow / failed. Partial success is normal. Requires the Pro plan or higher, or the paid option (402).",
        "x-scope": "editor",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Site id (UUID). The site must belong to the key's project, otherwise 404."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BulkEmailsInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BulkAddResult"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/UnsupportedMediaType"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/sites/{id}/access/emails/{email_id}": {
      "patch": {
        "tags": [
          "access"
        ],
        "operationId": "set_site_access_email_label",
        "summary": "Set an access email label",
        "description": "Changes the name shown in the log (≤60 characters; empty/null — clear). Unknown address — 404.",
        "x-scope": "editor",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Site id (UUID). The site must belong to the key's project, otherwise 404."
          },
          {
            "name": "email_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Address record id (from the address list)."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EmailLabelInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EmailLabelResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/UnsupportedMediaType"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "delete": {
        "tags": [
          "access"
        ],
        "operationId": "remove_site_access_email",
        "summary": "Remove one access email",
        "description": "Access is closed immediately: passes of this address are revoked. Deleting a non-existent id also returns 200 (see `remove_email`).",
        "x-scope": "editor",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Site id (UUID). The site must belong to the key's project, otherwise 404."
          },
          {
            "name": "email_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Address record id (from the address list)."
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OkNextResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/sites/{id}/access/emails/remove": {
      "post": {
        "tags": [
          "access"
        ],
        "operationId": "remove_site_access_emails",
        "summary": "Remove access emails in bulk or all",
        "description": "Requires `confirm:true` (otherwise 400). `all:true` clears the whole list; otherwise the addresses from `raw` are removed (empty list — 400). Not gated by plan.",
        "x-scope": "editor",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Site id (UUID). The site must belong to the key's project, otherwise 404."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RemoveEmailsInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BulkRemoveResult"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/UnsupportedMediaType"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/sites/{id}/access/members-mode": {
      "post": {
        "tags": [
          "members"
        ],
        "operationId": "set_site_members_access",
        "summary": "Switch to member registration mode",
        "description": "Visitors register themselves (email + code) and the owner moderates. Enables the `members` gate. No request body — settings are set separately (PATCH …/members-settings, PUT …/documents). Requires the Pro plan or higher, or the paid option (402).",
        "x-scope": "editor",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Site id (UUID). The site must belong to the key's project, otherwise 404."
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MembersModeResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/sites/{id}/members": {
      "get": {
        "tags": [
          "members"
        ],
        "operationId": "get_site_members",
        "summary": "List site members",
        "description": "List of members (up to 500, search by `q`), per-status counters, mode settings and documents.",
        "x-scope": "readonly",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Site id (UUID). The site must belong to the key's project, otherwise 404."
          },
          {
            "name": "q",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "default": ""
            },
            "description": "Search members."
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MembersListResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/sites/{id}/members/reconsent": {
      "post": {
        "tags": [
          "members"
        ],
        "operationId": "request_members_reconsent",
        "summary": "Request consent again",
        "description": "Existing members will re-confirm consent to the documents on their next login. Requires a plan with member registration (402). No request body.",
        "x-scope": "editor",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Site id (UUID). The site must belong to the key's project, otherwise 404."
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OkResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/sites/{id}/members/{member_id}/moderate": {
      "post": {
        "tags": [
          "members"
        ],
        "operationId": "moderate_site_member",
        "summary": "Moderate a member",
        "description": "One action: `approve` (approve, sends an “access granted” email), `reject` (reject, no email), `block` (sends an “access closed” email), `unblock`. Unknown action — 400. There is intentionally no member deletion via the API (deleting personal data is an owner action in the panel).",
        "x-scope": "editor",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Site id (UUID). The site must belong to the key's project, otherwise 404."
          },
          {
            "name": "member_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Member id."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ModerateInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ModerateResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/UnsupportedMediaType"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/sites/{id}/members-settings": {
      "patch": {
        "tags": [
          "members"
        ],
        "operationId": "set_site_members_settings",
        "summary": "Update member registration settings",
        "description": "Patch: an absent field is unchanged. Allowed values: name_label `name|fio`, consent_mode `button|checkbox`, form_style `single|steps`, max_seats 1–100000 or null (400 otherwise). If the mode is not enabled yet — 409. Requires the plan (402).",
        "x-scope": "editor",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Site id (UUID). The site must belong to the key's project, otherwise 404."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MembersSettingsInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OkResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/UnsupportedMediaType"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/sites/{id}/documents": {
      "put": {
        "tags": [
          "members"
        ],
        "operationId": "set_site_documents",
        "summary": "Set owner documents on the login screen",
        "description": "Full replacement of the document set (0–3; more — 400). All documents are validated before the first write (title ≤60 without markup; https:// link ≤512). Requires a plan with member registration (402).",
        "x-scope": "editor",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Site id (UUID). The site must belong to the key's project, otherwise 404."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DocumentsInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DocumentsResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/UnsupportedMediaType"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/sites/{id}/stats": {
      "get": {
        "tags": [
          "stats"
        ],
        "operationId": "get_site_stats",
        "summary": "Get site analytics: daily series and totals",
        "description": "Server-side analytics (no cookies, no scripts). `human_*` — after bot filters; without the prefix — all requests (plan traffic is counted by these). Plus form submissions and deploys per day and ready-made `totals`.",
        "x-scope": "readonly",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Site id (UUID). The site must belong to the key's project, otherwise 404."
          },
          {
            "name": "days",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 7,
              "minimum": 1,
              "maximum": 365
            },
            "description": "Period in days from today (Moscow time). Values outside 1–365 are silently clamped. Ignored if from or to is set."
          },
          {
            "name": "from",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date"
            },
            "description": "Start of the period, Moscow date inclusive (YYYY-MM-DD). Clamped to [today−364, today]."
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date"
            },
            "description": "End of the period, inclusive. Swapped bounds are reordered."
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StatsResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/sites/{id}/stats/tops": {
      "get": {
        "tags": [
          "stats"
        ],
        "operationId": "get_site_stats_tops",
        "summary": "Get top lists for a period",
        "description": "Pages, referrers, downloads, 404s, devices (top 20 per kind; rows with fewer than 5 hits are hidden to protect visitors of small sites) and bot filter reasons (`filtered`, no threshold).",
        "x-scope": "readonly",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Site id (UUID). The site must belong to the key's project, otherwise 404."
          },
          {
            "name": "days",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 7,
              "minimum": 1,
              "maximum": 365
            },
            "description": "Period in days from today (Moscow time). Values outside 1–365 are silently clamped. Ignored if from or to is set."
          },
          {
            "name": "from",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date"
            },
            "description": "Start of the period, Moscow date inclusive (YYYY-MM-DD). Clamped to [today−364, today]."
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date"
            },
            "description": "End of the period, inclusive. Swapped bounds are reordered."
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TopsResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/sites/{id}/git": {
      "get": {
        "tags": [
          "git"
        ],
        "operationId": "get_site_git",
        "summary": "Get the repository connection",
        "description": "Current repository connection of the site (without secrets). If no repository is connected — 404.",
        "x-scope": "readonly",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Site id (UUID). The site must belong to the key's project, otherwise 404."
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SiteGit"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "put": {
        "tags": [
          "git"
        ],
        "operationId": "configure_site_git",
        "summary": "Connect or update a repository (generic)",
        "description": "Configures Git CD: https URL of the repository, branch (default main), optionally `auth_user`/`token` for a private repository (the token is encrypted and never returned) and build commands. The URL is checked against SSRF (https only, public addresses). A repeated PUT updates the connection.",
        "x-scope": "editor",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Site id (UUID). The site must belong to the key's project, otherwise 404."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/GitConfigInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SiteGit"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/UnsupportedMediaType"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "delete": {
        "tags": [
          "git"
        ],
        "operationId": "unlink_site_git",
        "summary": "Disconnect the repository",
        "description": "Removes the connection (and the webhook secret).",
        "x-scope": "editor",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Site id (UUID). The site must belong to the key's project, otherwise 404."
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OkResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/sites/{id}/git/deploy": {
      "post": {
        "tags": [
          "git"
        ],
        "operationId": "deploy_site_git",
        "summary": "Deploy from the repository",
        "description": "Manual trigger: the build-runner clones the current HEAD of the branch (from the body or the live branch of the connection) and builds/publishes it. No repository connected — 400. Gates: storage quota (409), deploy rate / build minutes (429), suspension (402). The body is required (may be `{}`).",
        "x-scope": "editor",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Site id (UUID). The site must belong to the key's project, otherwise 404."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/GitDeployInput"
              },
              "example": {
                "branch": "main"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The deploy is queued (source_kind=git, status=queued).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Deploy"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/UnsupportedMediaType"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/sites/{id}/git/webhook": {
      "post": {
        "tags": [
          "git"
        ],
        "operationId": "enable_site_git_webhook",
        "summary": "Enable the push-to-deploy webhook",
        "description": "(Re)generates the webhook secret and returns the URL + secret to configure at the git provider (GitVerse — Authorization: Bearer <secret>; GitLab — Secret token; GitHub/Gitea — X-Hub-Signature-256). The secret is shown ONCE. No repository connected — 400. No request body.",
        "x-scope": "editor",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Site id (UUID). The site must belong to the key's project, otherwise 404."
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "apiKey": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "tqk_<prefix>_<secret>",
        "description": "Project API key in the `tqk_<prefix>_<secret>` format — issued in the panel app.tuqo.ru in the project's “API keys” section (shown once). Passed in the `Authorization: Bearer tqk_...` header. Key scopes: `full` (everything, including deleting sites and domains), `editor` (create/update/deploy, without deleting sites and domains), `readonly` (read only). The scope required by each operation is in the `x-scope` extension. A key is bound to ONE project; projects, keys, members and billing are managed only in the panel."
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "properties": {
          "message": {
            "type": "string",
            "description": "Human-readable error message (the API returns it in Russian)."
          }
        },
        "required": [
          "message"
        ],
        "description": "Common REST API error format: `{\"message\": \"...\"}`. Codes: 400 — invalid data (validation), 401 — key missing/invalid/revoked/expired, 402 — the feature or limit requires a paid plan/option, 403 — the key lacks the required scope, 404 — resource not found or belongs to another project (resources of other projects are indistinguishable from missing ones), 409 — state conflict (subdomain taken, storage quota, deploy cannot be activated, etc.), 429 — rate limit exceeded (deploys per day, build minutes, DNS checks), 500 — internal error, 503 — service/storage unavailable.",
        "example": {
          "message": "not found"
        }
      },
      "ManifestMissingError": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string",
            "description": "Error text."
          },
          "missing": {
            "type": "array",
            "items": {
              "type": "string",
              "description": "sha256 (hex, lowercase)"
            },
            "description": "Hashes of blobs the project does not have yet — upload them via PUT /api/v1/blobs/{sha256}."
          }
        },
        "required": [
          "error",
          "missing"
        ],
        "description": "Special 400 format for POST /api/v1/sites/{id}/deploys/manifest when not all blobs are uploaded (the `error` field instead of `message`).",
        "example": {
          "error": "not all blobs are uploaded — upload the missing ones first via PUT /api/v1/blobs/{sha256}",
          "missing": [
            "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
          ]
        }
      },
      "Scope": {
        "type": "string",
        "enum": [
          "full",
          "editor",
          "readonly"
        ],
        "description": "API key scope. Hierarchy: `full` ⊃ `editor` ⊃ `readonly`. Read operations are available to any scope, changes — to `editor` and `full`, deleting a site or domain — only to `full`."
      },
      "Whoami": {
        "type": "object",
        "properties": {
          "project_id": {
            "type": "string",
            "format": "uuid",
            "description": "Project the key is bound to. All operations are scoped to it."
          },
          "project_name": {
            "type": [
              "string",
              "null"
            ],
            "description": "Project name (null if the project is not found)."
          },
          "scopes": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Scope"
            },
            "description": "Key scopes (as recorded in the key)."
          }
        },
        "required": [
          "project_id",
          "project_name",
          "scopes"
        ],
        "example": {
          "project_id": "7f7b8a2e-3c7e-4d5a-9c1b-2f0b7c0a1e11",
          "project_name": "My project",
          "scopes": [
            "full"
          ]
        }
      },
      "Finding": {
        "type": "object",
        "properties": {
          "code": {
            "type": "string",
            "description": "Machine-readable finding code."
          },
          "level": {
            "type": "string",
            "enum": [
              "fix",
              "improve"
            ],
            "description": "`fix` — worth fixing, `improve` — a suggestion."
          },
          "title": {
            "type": "string"
          },
          "why": {
            "type": "string",
            "description": "Why it matters."
          },
          "how": {
            "type": "string",
            "description": "How to fix it."
          },
          "where": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "File paths where it was found."
          }
        },
        "required": [
          "code",
          "level",
          "title",
          "why",
          "how",
          "where"
        ],
        "description": "A site checks recommendation item (`shared::checks::Finding`)."
      },
      "Recommendations": {
        "type": "object",
        "properties": {
          "summary": {
            "type": "object",
            "properties": {
              "fix": {
                "type": "integer"
              },
              "improve": {
                "type": "integer"
              }
            },
            "description": "Number of findings of each level in the whole report."
          },
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Finding"
            },
            "description": "First 5 items (most important first)."
          },
          "more": {
            "type": "integer",
            "description": "Number of items not shown."
          },
          "note": {
            "type": "string",
            "description": "Reminder: these are suggestions about files, not errors — the site is published and working."
          }
        },
        "required": [
          "summary",
          "items",
          "more",
          "note"
        ],
        "description": "Recommendations for the latest publish (`shared::checks::recommendations`). Present only if the site's `checks_enabled` switch is on and the report is not empty."
      },
      "Site": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "project_id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string"
          },
          "description": {
            "type": "string",
            "description": "Site description (empty string if none)."
          },
          "subdomain": {
            "type": "string",
            "description": "Platform subdomain: the site opens at `https://<subdomain>.tuqo.ru`."
          },
          "active_deploy_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "Current live deploy (null until one is activated)."
          },
          "enabled": {
            "type": "boolean",
            "description": "Manual serving switch (false → “temporarily unavailable”)."
          },
          "subdomain_enabled": {
            "type": "boolean",
            "description": "Whether the platform subdomain is served (custom domains keep working)."
          },
          "subdomain_redirect_enabled": {
            "type": "boolean",
            "description": "When the subdomain is disabled: true → 301 to a working address."
          },
          "subdomain_redirect_to": {
            "type": [
              "string",
              "null"
            ],
            "description": "Subdomain redirect target (host); null → automatic."
          },
          "access_mode": {
            "type": "string",
            "enum": [
              "public",
              "restricted"
            ],
            "description": "`restricted` — the site is gated (see the access group)."
          },
          "keep_deploys": {
            "type": "integer",
            "description": "How many deploy copies to keep (active + archived)."
          },
          "retention_days": {
            "type": "integer",
            "description": "Auto-delete archived copies older than N days (0 = off)."
          },
          "checks_enabled": {
            "type": "boolean",
            "description": "Whether to compute site checks (recommendations) on publish."
          },
          "deletion_scheduled_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Site in trash: time of the deletion request (null = active)."
          },
          "deletion_reason": {
            "type": "string",
            "description": "Deletion reason: \"owner\" — the site is active or was deleted by the owner, \"billing\" — scheduled for deletion for non-payment. Column TEXT NOT NULL DEFAULT 'owner': never an empty string.",
            "enum": [
              "owner",
              "billing"
            ]
          },
          "auto_disabled_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Site stopped automatically or by an operator (null = normal state)."
          },
          "disabled_reason": {
            "type": [
              "string",
              "null"
            ]
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "Live URL of the site `https://<subdomain>.<host>` (computed field `site_with_url`)."
          },
          "form_endpoint": {
            "type": "string",
            "format": "uri",
            "description": "Where the site form sends submissions: `https://api.<host>/f/<site_id>` (computed field)."
          },
          "recommendations": {
            "$ref": "#/components/schemas/Recommendations"
          }
        },
        "required": [
          "id",
          "project_id",
          "name",
          "description",
          "subdomain",
          "active_deploy_id",
          "enabled",
          "subdomain_enabled",
          "subdomain_redirect_enabled",
          "subdomain_redirect_to",
          "access_mode",
          "keep_deploys",
          "retention_days",
          "checks_enabled",
          "deletion_scheduled_at",
          "deletion_reason",
          "auto_disabled_at",
          "disabled_reason",
          "created_at",
          "updated_at",
          "url",
          "form_endpoint"
        ],
        "description": "Project site (`shared::model::Site` + computed `url`, `form_endpoint`). The `recommendations` field is added only in GET /api/v1/sites/{id}.",
        "example": {
          "id": "2b1d6a7e-0c1f-4b6e-9a2d-5f3c8e7d1a20",
          "project_id": "7f7b8a2e-3c7e-4d5a-9c1b-2f0b7c0a1e11",
          "name": "Portfolio",
          "description": "",
          "subdomain": "moe-portfolio",
          "active_deploy_id": "c0ffee00-1234-4abc-8def-0123456789ab",
          "enabled": true,
          "subdomain_enabled": true,
          "subdomain_redirect_enabled": false,
          "subdomain_redirect_to": null,
          "access_mode": "public",
          "keep_deploys": 3,
          "retention_days": 0,
          "checks_enabled": true,
          "deletion_scheduled_at": null,
          "deletion_reason": "owner",
          "auto_disabled_at": null,
          "disabled_reason": null,
          "created_at": "2026-09-01T10:00:00Z",
          "updated_at": "2026-09-01T10:00:00Z",
          "url": "https://moe-portfolio.tuqo.ru",
          "form_endpoint": "https://api.tuqo.ru/f/2b1d6a7e-0c1f-4b6e-9a2d-5f3c8e7d1a20"
        }
      },
      "CreateSiteInput": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "Site name (required, not empty after trim)."
          },
          "subdomain": {
            "type": "string",
            "minLength": 5,
            "maxLength": 40,
            "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?$",
            "description": "Subdomain: 5–40 characters, lowercase Latin letters, digits and hyphens; no leading/trailing hyphen and no `--`; lowercased. Reserved names (www, api, app, admin, login, …) → 409. Names mimicking brands or government bodies → 400. Shorter than 9 characters — paid plans only (400)."
          }
        },
        "required": [
          "name",
          "subdomain"
        ],
        "example": {
          "name": "Portfolio",
          "subdomain": "moe-portfolio"
        }
      },
      "UpdateSiteInput": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "New name (required, not empty)."
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "description": "Description. If the field is absent, it is unchanged."
          }
        },
        "required": [
          "name"
        ],
        "example": {
          "name": "Portfolio 2026",
          "description": "Personal website"
        }
      },
      "SetEnabledInput": {
        "type": "object",
        "properties": {
          "enabled": {
            "type": "boolean",
            "description": "true — enable serving, false — disable."
          }
        },
        "required": [
          "enabled"
        ],
        "example": {
          "enabled": false
        }
      },
      "Deploy": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "site_id": {
            "type": "string",
            "format": "uuid"
          },
          "status": {
            "type": "string",
            "enum": [
              "queued",
              "building",
              "ready",
              "active",
              "failed",
              "superseded"
            ],
            "description": "Lifecycle: queued → building → ready → active (or failed); replaced as live — superseded."
          },
          "source_ref": {
            "type": "string",
            "description": "S3 key of the sources/manifest (internal)."
          },
          "source_kind": {
            "type": "string",
            "description": "How it was created: \"sources\" — a full file set or archive (including deploy-files), \"manifest\" — a manifest deploy from blobs, \"git\" — Git CD. The value \"dist\" is mentioned in a model comment but never assigned by code (legacy).",
            "enum": [
              "sources",
              "manifest",
              "git"
            ]
          },
          "build_mode": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "static",
              "build",
              null
            ],
            "description": "How it was actually published (written by the build-runner): `static` — as is, no build minutes; `build` — a build ran. null until it reaches ready."
          },
          "commit_sha": {
            "type": [
              "string",
              "null"
            ]
          },
          "size_bytes": {
            "type": "integer"
          },
          "artifacts_purged": {
            "type": "boolean",
            "description": "Artifacts were purged by retention or deletion — the copy cannot be activated."
          },
          "error": {
            "type": [
              "string",
              "null"
            ],
            "description": "Error text when status=failed."
          },
          "created_by": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "Creating user (null for an API key)."
          },
          "created_via": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "panel",
              "api_key",
              "oauth",
              null
            ]
          },
          "creator_label": {
            "type": [
              "string",
              "null"
            ],
            "description": "Key name / `tqk_…` prefix or OAuth app name."
          },
          "branch": {
            "type": [
              "string",
              "null"
            ],
            "description": "Branch (for source_kind=git)."
          },
          "is_preview": {
            "type": "boolean",
            "description": "Built without publishing (`activate:false`) — the live URL was not changed."
          },
          "has_preview_link": {
            "type": "boolean",
            "description": "Whether a preview link has been issued (the secret itself is not returned)."
          },
          "pr_number": {
            "type": [
              "integer",
              "null"
            ]
          },
          "skipped_paths": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Service paths that were not published (`.git`, `.env`, `/__tuqo/…`); the list is truncated."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "started_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "finished_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "recommendations": {
            "$ref": "#/components/schemas/Recommendations"
          }
        },
        "required": [
          "id",
          "site_id",
          "status",
          "source_ref",
          "source_kind",
          "build_mode",
          "commit_sha",
          "size_bytes",
          "artifacts_purged",
          "error",
          "created_by",
          "created_via",
          "creator_label",
          "branch",
          "is_preview",
          "has_preview_link",
          "pr_number",
          "skipped_paths",
          "created_at",
          "started_at",
          "finished_at"
        ],
        "description": "Site deploy (`shared::model::Deploy`). The `recommendations` field appears only in deploy-files and deploys/manifest responses when checks are enabled and there are findings.",
        "example": {
          "id": "c0ffee00-1234-4abc-8def-0123456789ab",
          "site_id": "2b1d6a7e-0c1f-4b6e-9a2d-5f3c8e7d1a20",
          "status": "queued",
          "source_ref": "",
          "source_kind": "manifest",
          "build_mode": null,
          "commit_sha": null,
          "size_bytes": 0,
          "artifacts_purged": false,
          "error": null,
          "created_by": null,
          "created_via": "api_key",
          "creator_label": "ci-key",
          "branch": null,
          "is_preview": false,
          "has_preview_link": false,
          "pr_number": null,
          "skipped_paths": [],
          "created_at": "2026-09-07T12:00:00Z",
          "started_at": null,
          "finished_at": null
        }
      },
      "FileInput": {
        "type": "object",
        "properties": {
          "path": {
            "type": "string",
            "maxLength": 1024,
            "description": "Relative file path inside the site (`index.html`, `css/app.css`). Forbidden: absolute paths, `..`, backslashes, control characters, empty components. `index.html` at the root is required; `package.json` at the root → 400 (that is a build project). Files under `/__tuqo/` are filtered out by the platform."
          },
          "content": {
            "type": "string",
            "description": "Content: text as is (encoding=utf8) or base64 (encoding=base64). Required if `sha256` is absent."
          },
          "encoding": {
            "type": "string",
            "enum": [
              "utf8",
              "text",
              "base64"
            ],
            "default": "utf8",
            "description": "Encoding of `content`. Any other value → 400."
          },
          "sha256": {
            "type": "string",
            "pattern": "^[0-9a-fA-F]{64}$",
            "description": "Instead of `content`: a reference to an already uploaded blob of the PROJECT (from GET /sites/{id}/manifest or after PUT /blobs). A hash from another project or an unknown hash → 400."
          }
        },
        "required": [
          "path"
        ],
        "description": "One file of the set (`shared::deploy_assets::FileInput`): either `content` (+`encoding`) or `sha256`."
      },
      "DeployFilesInput": {
        "type": "object",
        "properties": {
          "files": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FileInput"
            },
            "minItems": 1,
            "maxItems": 2000,
            "description": "The FULL set of site files (not a patch). Empty list → 400, more than 2000 → 400."
          },
          "activate": {
            "type": "boolean",
            "default": true,
            "description": "false — build WITHOUT publishing: the live URL stays on the previous version; POST /deploys/{id}/preview returns a link (requires a plan with preview links, otherwise 402)."
          }
        },
        "required": [
          "files"
        ],
        "description": "Body of POST /api/v1/sites/{id}/deploy-files. The request body is limited to 50 MB (route DefaultBodyLimit). The `idempotency_key` field is NOT processed in REST (MCP only) — unknown fields are ignored.",
        "example": {
          "files": [
            {
              "path": "index.html",
              "content": "<!doctype html><title>Hello</title>"
            },
            {
              "path": "img/logo.png",
              "content": "iVBORw0KGgo...",
              "encoding": "base64"
            },
            {
              "path": "img/photo.jpg",
              "sha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
            }
          ],
          "activate": true
        }
      },
      "ManifestEntry": {
        "type": "object",
        "properties": {
          "path": {
            "type": "string",
            "description": "Relative file path (same rules as FileInput.path)."
          },
          "sha256": {
            "type": "string",
            "pattern": "^[0-9a-fA-F]{64}$",
            "description": "sha256 of the file content in hex (64 characters; case-insensitive, normalized to lowercase)."
          },
          "size": {
            "type": "integer",
            "description": "Size in bytes — optional and ignored by the server (the limit is computed from actual blob sizes)."
          }
        },
        "required": [
          "path",
          "sha256"
        ]
      },
      "ManifestInput": {
        "type": "object",
        "properties": {
          "files": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ManifestEntry"
            },
            "minItems": 1,
            "maxItems": 2000
          },
          "activate": {
            "type": "boolean",
            "default": true,
            "description": "false — build without publishing (see DeployFilesInput.activate)."
          }
        },
        "required": [
          "files"
        ],
        "example": {
          "files": [
            {
              "path": "index.html",
              "sha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
              "size": 1234
            }
          ],
          "activate": true
        }
      },
      "CheckResponse": {
        "type": "object",
        "properties": {
          "missing": {
            "type": "array",
            "items": {
              "type": "string",
              "description": "sha256, lowercase"
            },
            "description": "Hashes the project does not have — upload them via PUT /api/v1/blobs/{sha256}. Empty list — POST …/deploys/manifest can be called right away."
          }
        },
        "required": [
          "missing"
        ],
        "example": {
          "missing": [
            "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
          ]
        }
      },
      "BlobPutResponse": {
        "type": "object",
        "properties": {
          "stored": {
            "type": "boolean",
            "description": "Always true on success."
          },
          "sha256": {
            "type": "string",
            "description": "Hash in lowercase."
          }
        },
        "required": [
          "stored",
          "sha256"
        ],
        "example": {
          "stored": true,
          "sha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
        }
      },
      "ManifestFile": {
        "type": "object",
        "properties": {
          "path": {
            "type": "string"
          },
          "sha256": {
            "type": "string"
          }
        },
        "required": [
          "path",
          "sha256"
        ]
      },
      "ManifestResponse": {
        "type": "object",
        "properties": {
          "manifest": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ManifestFile"
            },
            "description": "Files of the active deploy. Empty array if there is no active deploy/manifest (for example, the site was built from an archive)."
          }
        },
        "required": [
          "manifest"
        ],
        "example": {
          "manifest": [
            {
              "path": "index.html",
              "sha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
            }
          ]
        }
      },
      "ActivateResponse": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean"
          },
          "site": {
            "$ref": "#/components/schemas/Site",
            "description": "Updated site (new active_deploy_id)."
          }
        },
        "required": [
          "ok",
          "site"
        ]
      },
      "PreviewResponse": {
        "type": "object",
        "properties": {
          "preview_url": {
            "type": "string",
            "format": "uri",
            "description": "Secret link of the form `https://preview.tuqo.ru/<token>/`. Opens without a password even on a gated site."
          }
        },
        "required": [
          "preview_url"
        ],
        "example": {
          "preview_url": "https://preview.tuqo.ru/0123456789abcdef0123456789abcdef/"
        }
      },
      "OkResponse": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean",
            "description": "Always true."
          }
        },
        "required": [
          "ok"
        ],
        "example": {
          "ok": true
        }
      },
      "OkNextResponse": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean"
          },
          "next_action": {
            "type": "string",
            "description": "Next-step hint for the agent."
          }
        },
        "required": [
          "ok",
          "next_action"
        ]
      },
      "Domain": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "site_id": {
            "type": "string",
            "format": "uuid"
          },
          "domain": {
            "type": "string",
            "description": "FQDN in lowercase."
          },
          "verified": {
            "type": "boolean",
            "description": "Ownership verified by the TXT record."
          },
          "enabled": {
            "type": "boolean",
            "description": "Whether the domain is served."
          },
          "redirect_enabled": {
            "type": "boolean"
          },
          "redirect_to": {
            "type": [
              "string",
              "null"
            ]
          },
          "verification_token": {
            "type": "string",
            "description": "Value of the `_tuqo-verify.<domain>` TXT record."
          },
          "ssl_status": {
            "type": "string",
            "enum": [
              "active",
              "pending",
              "failed"
            ],
            "description": "Certificate status from the last probe."
          },
          "ssl_checked_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "a_record_ok": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "A record diagnostics: true — points to the serving IPs; false — missing or points elsewhere; null — not checked. Does not affect `verified`."
          },
          "a_record_found": {
            "type": [
              "string",
              "null"
            ],
            "description": "Found A records, comma-separated."
          },
          "a_checked_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "verified_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        },
        "required": [
          "id",
          "site_id",
          "domain",
          "verified",
          "enabled",
          "redirect_enabled",
          "redirect_to",
          "verification_token",
          "ssl_status",
          "ssl_checked_at",
          "a_record_ok",
          "a_record_found",
          "a_checked_at",
          "created_at",
          "verified_at"
        ],
        "description": "Custom domain of a site (`shared::model::Domain`)."
      },
      "AddDomainInput": {
        "type": "object",
        "properties": {
          "domain": {
            "type": "string",
            "maxLength": 253,
            "description": "FQDN (ASCII, lowercased): ≥2 dot-separated labels, each 1–63 characters `[a-z0-9-]` without leading/trailing hyphen. Punycode/IDN must be passed already in ASCII. Mimicking brands or government bodies → 400."
          }
        },
        "required": [
          "domain"
        ],
        "example": {
          "domain": "example.ru"
        }
      },
      "DomainVerifyHint": {
        "type": "object",
        "properties": {
          "type": {
            "type": "string",
            "const": "TXT"
          },
          "name": {
            "type": "string",
            "description": "Record name: `_tuqo-verify.<domain>`."
          },
          "value": {
            "type": "string",
            "description": "Value = verification_token."
          }
        },
        "required": [
          "type",
          "name",
          "value"
        ]
      },
      "DomainAddResponse": {
        "type": "object",
        "properties": {
          "domain": {
            "$ref": "#/components/schemas/Domain"
          },
          "verify": {
            "$ref": "#/components/schemas/DomainVerifyHint",
            "description": "Which TXT record to create in DNS."
          }
        },
        "required": [
          "domain",
          "verify"
        ]
      },
      "ARecordDiagnostics": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean",
            "description": "All found A records point to Tuqo serving IPs."
          },
          "found": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Found A records."
          },
          "expected": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Tuqo serving IPs the A record should point to."
          },
          "message": {
            "type": "string",
            "description": "Human-readable explanation."
          }
        },
        "required": [
          "ok",
          "found",
          "expected",
          "message"
        ],
        "description": "A record diagnostics (`dns::a_diagnostics`). Does not affect `verified`, but shows whether the site will open on the domain."
      },
      "DomainVerifyResponse": {
        "type": "object",
        "properties": {
          "verified": {
            "type": "boolean"
          },
          "domain": {
            "$ref": "#/components/schemas/Domain",
            "description": "Present when verified=true."
          },
          "message": {
            "type": "string",
            "description": "Present when verified=false: “TXT record not found or does not match”."
          },
          "a_record": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/ARecordDiagnostics"
              },
              {
                "type": "null"
              }
            ],
            "description": "null if DNS resolution failed or, for an already verified domain, the throttle (10 s) kicked in."
          }
        },
        "required": [
          "verified"
        ],
        "description": "Domain verification response. For an already verified domain the call re-checks the A record (no 429).",
        "example": {
          "verified": false,
          "message": "TXT record not found or does not match",
          "a_record": {
            "ok": false,
            "found": [],
            "expected": [
              "201.51.6.16"
            ],
            "message": "A record not found"
          }
        }
      },
      "FormSiteInfo": {
        "type": "object",
        "properties": {
          "site_id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string"
          },
          "subdomain": {
            "type": "string"
          },
          "honeypot": {
            "type": [
              "string",
              "null"
            ],
            "description": "Name of the form's honeypot field (if set)."
          },
          "min_submit_ms": {
            "type": "integer",
            "description": "Minimum form fill time (anti-bot)."
          }
        },
        "required": [
          "site_id",
          "name",
          "subdomain",
          "honeypot",
          "min_submit_ms"
        ]
      },
      "FormsOverview": {
        "type": "object",
        "properties": {
          "available": {
            "type": "boolean",
            "description": "Capacity to accept submissions is available (monthly quota > 0 or purchased submissions exist)."
          },
          "monthly_quota": {
            "type": "integer",
            "description": "Monthly submission quota of the plan."
          },
          "monthly_used": {
            "type": "integer"
          },
          "lead_balance": {
            "type": "integer",
            "description": "Purchased submissions (packs)."
          },
          "hidden_count": {
            "type": "integer",
            "description": "Submissions accepted over the limit — their content is hidden until payment."
          },
          "sites": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FormSiteInfo"
            },
            "description": "Project sites (not in trash) with form settings."
          },
          "counts": {
            "type": "object",
            "properties": {
              "verified": {
                "type": "integer"
              },
              "spam": {
                "type": "integer"
              },
              "archive": {
                "type": "integer"
              },
              "unread": {
                "type": "integer"
              }
            },
            "required": [
              "verified",
              "spam",
              "archive",
              "unread"
            ]
          }
        },
        "required": [
          "available",
          "monthly_quota",
          "monthly_used",
          "lead_balance",
          "hidden_count",
          "sites",
          "counts"
        ]
      },
      "FormSubmission": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "site_id": {
            "type": "string",
            "format": "uuid"
          },
          "project_id": {
            "type": "string",
            "format": "uuid"
          },
          "data": {
            "type": "object",
            "additionalProperties": true,
            "description": "Form fields as sent by the site. For hidden submissions (`hidden=true`) — an empty object `{}`."
          },
          "ip": {
            "type": [
              "string",
              "null"
            ]
          },
          "origin": {
            "type": [
              "string",
              "null"
            ]
          },
          "is_spam": {
            "type": "boolean"
          },
          "spam_reason": {
            "type": [
              "string",
              "null"
            ]
          },
          "hidden": {
            "type": "boolean",
            "description": "Content is hidden (accepted over the quota on a paid plan)."
          },
          "quota_source": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "monthly",
              "lead",
              null
            ]
          },
          "read_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "archived_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "unhidden_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "autoreply_status": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "pending",
              "sent",
              "no_email_field",
              "invalid_email",
              "address_limited",
              "site_limited",
              "plan_disabled",
              "send_failed",
              null
            ],
            "description": "Auto-reply status for the submitter (null = not applicable)."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "id",
          "site_id",
          "project_id",
          "data",
          "ip",
          "origin",
          "is_spam",
          "spam_reason",
          "hidden",
          "quota_source",
          "read_at",
          "archived_at",
          "unhidden_at",
          "autoreply_status",
          "created_at"
        ],
        "description": "Form submission (`shared::model::FormSubmission`)."
      },
      "SubmissionsResponse": {
        "type": "object",
        "properties": {
          "submissions": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FormSubmission"
            }
          },
          "status": {
            "type": "string",
            "enum": [
              "verified",
              "spam",
              "archive"
            ],
            "description": "Applied status filter."
          }
        },
        "required": [
          "submissions",
          "status"
        ]
      },
      "SiteAutoreply": {
        "type": "object",
        "properties": {
          "site_id": {
            "type": "string",
            "format": "uuid"
          },
          "enabled": {
            "type": "boolean"
          },
          "subject": {
            "type": "string",
            "description": "Email subject (up to 120 characters)."
          },
          "body": {
            "type": "string",
            "description": "Email body (up to 4096 characters). Supports markup (**bold**, lists, quotes, [links](https://…)) and {{field_name}} placeholders from the submission."
          },
          "email_field": {
            "type": [
              "string",
              "null"
            ],
            "description": "Name of the form field with the submitter's address (null = heuristic)."
          },
          "link_ttl_days": {
            "type": "integer",
            "description": "Delivery link lifetime, days. The server clamps the value to 1–60.",
            "minimum": 1,
            "maximum": 60
          },
          "upsell_text": {
            "type": "string",
            "description": "Description on the delivery page."
          },
          "page_title": {
            "type": [
              "string",
              "null"
            ],
            "description": "Delivery page title."
          },
          "has_page_logo": {
            "type": "boolean"
          },
          "page_logo_framed": {
            "type": "boolean"
          },
          "page_logo_rounded": {
            "type": "boolean"
          },
          "download_count": {
            "type": "integer",
            "description": "How many times the delivery file was downloaded."
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "site_id",
          "enabled",
          "subject",
          "body",
          "email_field",
          "link_ttl_days",
          "upsell_text",
          "page_title",
          "has_page_logo",
          "page_logo_framed",
          "page_logo_rounded",
          "download_count",
          "updated_at"
        ],
        "description": "Auto-reply settings (`shared::model::SiteAutoreply`)."
      },
      "LeadFile": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "file_name": {
            "type": "string",
            "description": "File name for download."
          },
          "label": {
            "type": "string",
            "description": "Label shown on the delivery page."
          },
          "mime": {
            "type": "string"
          },
          "bytes": {
            "type": "integer"
          }
        },
        "required": [
          "id",
          "file_name",
          "label",
          "mime",
          "bytes"
        ],
        "description": "Delivery file (lead magnet), up to three per site."
      },
      "AutoreplyView": {
        "type": "object",
        "properties": {
          "allowed": {
            "type": "boolean",
            "description": "Whether auto-reply is allowed by the owner's plan (from the Start plan)."
          },
          "upgrade_hint": {
            "type": "string",
            "description": "Hint about which plan to upgrade to (empty string if allowed)."
          },
          "settings": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/SiteAutoreply"
              },
              {
                "type": "null"
              }
            ],
            "description": "null if auto-reply has never been configured."
          },
          "files": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/LeadFile"
            }
          },
          "sent_this_month": {
            "type": "integer",
            "description": "Auto-replies sent this month."
          }
        },
        "required": [
          "allowed",
          "upgrade_hint",
          "settings",
          "files",
          "sent_this_month"
        ],
        "description": "Common response of all auto-reply endpoints (GET and all mutating ones)."
      },
      "AutoreplyPut": {
        "type": "object",
        "properties": {
          "enabled": {
            "type": "boolean",
            "description": "Enable auto-reply. Enabling requires the Start plan or higher (otherwise 402); disabling or editing the text is allowed on any plan."
          },
          "subject": {
            "type": "string",
            "maxLength": 120,
            "default": ""
          },
          "body": {
            "type": "string",
            "maxLength": 4096,
            "default": "",
            "description": "Markup and {{field_name}} placeholders."
          },
          "email_field": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 64,
            "description": "Name of the form field with the address; empty/null → heuristic."
          }
        },
        "required": [
          "enabled"
        ],
        "example": {
          "enabled": true,
          "subject": "Thank you for your request",
          "body": "Hello, {{name}}! We have received your request.",
          "email_field": "email"
        }
      },
      "FileLabelInput": {
        "type": "object",
        "properties": {
          "label": {
            "type": "string",
            "maxLength": 80,
            "description": "File label; an absent field = empty label."
          }
        },
        "example": {
          "label": "Checklist"
        }
      },
      "DeliveryPut": {
        "type": "object",
        "properties": {
          "link_ttl_days": {
            "type": "integer",
            "description": "Delivery link lifetime, days. The server clamps the value to 1–60.",
            "minimum": 1,
            "maximum": 60
          },
          "upsell_text": {
            "type": "string",
            "maxLength": 500,
            "default": "",
            "description": "Description on the delivery page. Set ONLY together with page_title (both or neither, otherwise 400)."
          },
          "page_title": {
            "type": "string",
            "maxLength": 80,
            "default": "",
            "description": "Delivery page title. Cannot be cleared while a logo is uploaded (400)."
          },
          "page_logo_framed": {
            "type": "boolean",
            "default": true
          },
          "page_logo_rounded": {
            "type": "boolean",
            "default": false
          }
        },
        "required": [
          "link_ttl_days"
        ],
        "example": {
          "link_ttl_days": 7,
          "page_title": "Your guide is ready",
          "upsell_text": "Download the file using the button below",
          "page_logo_framed": true,
          "page_logo_rounded": false
        }
      },
      "AccessState": {
        "type": "object",
        "properties": {
          "enabled": {
            "type": "boolean",
            "description": "Whether the gate is on. When false, the other fields are absent."
          },
          "method": {
            "type": "string",
            "enum": [
              "password",
              "email_list",
              "subscribers",
              "members",
              "paywall"
            ],
            "description": "Gate mode."
          },
          "password_level": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "easy",
              "medium",
              "strong",
              "custom",
              null
            ],
            "description": "Only for method=password, otherwise null."
          },
          "expires_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "expires_in_secs": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Remaining time in seconds (null — no expiry)."
          },
          "max_opens": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Opens limit (devices); null — no limit."
          },
          "opens_count": {
            "type": "integer"
          },
          "opens_left": {
            "type": [
              "integer",
              "null"
            ]
          },
          "last_opened_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "enabled"
        ],
        "description": "Gate state (`shared::access::state_json`). Contains no secrets (password hash, cookie secret).",
        "example": {
          "enabled": true,
          "method": "password",
          "password_level": "medium",
          "expires_at": null,
          "expires_in_secs": null,
          "max_opens": null,
          "opens_count": 2,
          "opens_left": null,
          "last_opened_at": "2026-09-06T18:00:00Z",
          "created_at": "2026-09-01T10:00:00Z"
        }
      },
      "AccessBrand": {
        "type": "object",
        "properties": {
          "brand_title": {
            "type": [
              "string",
              "null"
            ],
            "description": "Login screen title (up to 60 characters)."
          },
          "brand_text": {
            "type": [
              "string",
              "null"
            ],
            "description": "Description (up to 200 characters)."
          },
          "has_logo": {
            "type": "boolean"
          },
          "logo_framed": {
            "type": "boolean"
          },
          "logo_rounded": {
            "type": "boolean"
          },
          "logo_mime": {
            "type": [
              "string",
              "null"
            ]
          },
          "logo_width": {
            "type": [
              "integer",
              "null"
            ]
          },
          "logo_height": {
            "type": [
              "integer",
              "null"
            ]
          },
          "logo_bytes": {
            "type": [
              "integer",
              "null"
            ]
          },
          "favicon_mode": {
            "type": "string",
            "enum": [
              "none",
              "logo",
              "custom"
            ]
          },
          "has_favicon": {
            "type": "boolean"
          },
          "favicon_mime": {
            "type": [
              "string",
              "null"
            ]
          },
          "favicon_bytes": {
            "type": [
              "integer",
              "null"
            ]
          },
          "updated_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        },
        "required": [
          "brand_title",
          "brand_text",
          "has_logo",
          "logo_framed",
          "logo_rounded",
          "logo_mime",
          "logo_width",
          "logo_height",
          "logo_bytes",
          "favicon_mode",
          "has_favicon",
          "favicon_mime",
          "favicon_bytes",
          "updated_at"
        ],
        "description": "Login screen branding (`shared::access::brand_json`). Survives opening the access."
      },
      "GateStats": {
        "type": "object",
        "properties": {
          "views": {
            "type": "integer",
            "description": "Login screen views."
          },
          "passed": {
            "type": "integer",
            "description": "Logged in (pass issued)."
          },
          "failed": {
            "type": "integer",
            "description": "Wrong password / the bot replied “not subscribed”."
          },
          "checks": {
            "type": "integer",
            "description": "Started a subscription check (channel subscribers mode)."
          }
        },
        "required": [
          "views",
          "passed",
          "failed",
          "checks"
        ]
      },
      "AccessChannel": {
        "type": "object",
        "properties": {
          "channel_id": {
            "type": "string",
            "format": "uuid"
          },
          "platform": {
            "type": "string",
            "description": "Channel platform: telegram or max.",
            "enum": [
              "telegram",
              "max"
            ]
          },
          "kind": {
            "type": "string",
            "enum": [
              "channel",
              "group"
            ],
            "description": "Type: channel or group."
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "active",
              "error"
            ],
            "description": "Connection state: pending (awaiting confirmation), active, error."
          },
          "title": {
            "type": "string"
          },
          "link": {
            "type": "string"
          }
        },
        "required": [
          "channel_id",
          "platform",
          "kind",
          "status",
          "title",
          "link"
        ],
        "description": "Channel of the subscribers mode (`CheckChannelRow`)."
      },
      "AccessGetResponse": {
        "type": "object",
        "properties": {
          "access": {
            "$ref": "#/components/schemas/AccessState"
          },
          "brand": {
            "$ref": "#/components/schemas/AccessBrand"
          },
          "gate_stats_30d": {
            "$ref": "#/components/schemas/GateStats",
            "description": "Gate events for the last 30 days."
          },
          "channels": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AccessChannel"
            }
          }
        },
        "required": [
          "access",
          "brand",
          "gate_stats_30d",
          "channels"
        ]
      },
      "SetPasswordInput": {
        "type": "object",
        "properties": {
          "level": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "easy",
              "medium",
              "strong",
              null
            ],
            "description": "Level of the generated password; default medium. The value `custom` → 400 (pass your own password in `password`)."
          },
          "password": {
            "type": [
              "string",
              "null"
            ],
            "minLength": 6,
            "maxLength": 128,
            "description": "Custom password (6–128 characters). If set, `level` is ignored."
          }
        },
        "example": {
          "level": "strong"
        }
      },
      "SetPasswordResponse": {
        "type": "object",
        "properties": {
          "password": {
            "type": "string",
            "description": "Plaintext password — hand it to the user (later available via GET …/access/password)."
          },
          "access": {
            "$ref": "#/components/schemas/AccessState"
          },
          "brand": {
            "$ref": "#/components/schemas/AccessBrand"
          },
          "next_action": {
            "type": "string"
          }
        },
        "required": [
          "password",
          "access",
          "brand",
          "next_action"
        ]
      },
      "RevealPasswordResponse": {
        "type": "object",
        "properties": {
          "password": {
            "type": [
              "string",
              "null"
            ],
            "description": "Current password. null — the password was set before reversible storage was introduced: nothing to show, generate a new one."
          }
        },
        "required": [
          "password"
        ]
      },
      "BrandInput": {
        "type": "object",
        "properties": {
          "title": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 60,
            "description": "Login screen title. Absent — unchanged; null — clear."
          },
          "text": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 200,
            "description": "Description. Absent — unchanged; null — clear."
          },
          "logo_base64": {
            "type": [
              "string",
              "null"
            ],
            "description": "Logo in base64 (a data-URI prefix is allowed). PNG/JPEG/WebP up to 1 MB; SVG is rejected. null — remove the logo; absent — unchanged."
          },
          "logo_framed": {
            "type": "boolean",
            "description": "Backdrop under the logo. Absent — unchanged (for a new logo it is chosen by transparency)."
          },
          "logo_rounded": {
            "type": "boolean",
            "description": "Rounded logo corners. Absent — unchanged."
          }
        },
        "example": {
          "title": "Gallery for Anna",
          "text": "Enter the password from the email",
          "logo_framed": true
        }
      },
      "BrandResponse": {
        "type": "object",
        "properties": {
          "access": {
            "$ref": "#/components/schemas/AccessState"
          },
          "brand": {
            "$ref": "#/components/schemas/AccessBrand"
          }
        },
        "required": [
          "access",
          "brand"
        ]
      },
      "ExpiryInput": {
        "type": "object",
        "properties": {
          "days": {
            "type": [
              "integer",
              "null"
            ],
            "enum": [
              1,
              3,
              7,
              30,
              null
            ],
            "description": "Extend by a preset number of days from max(now, current expiry). Only 1, 3, 7 or 30."
          },
          "until": {
            "type": [
              "string",
              "null"
            ],
            "description": "Arbitrary moment in the project's time zone: `YYYY-MM-DD` (until the end of the day) or `YYYY-MM-DD HH:MM[:SS]` / `YYYY-MM-DDTHH:MM`. At most 366 days ahead."
          },
          "clear": {
            "type": "boolean",
            "default": false,
            "description": "true — remove the expiry (no limit). Mutually exclusive with days/until: both days and until at once → 400; nothing set → 400."
          }
        },
        "example": {
          "days": 7
        }
      },
      "AccessNextResponse": {
        "type": "object",
        "properties": {
          "access": {
            "$ref": "#/components/schemas/AccessState"
          },
          "next_action": {
            "type": "string"
          }
        },
        "required": [
          "access",
          "next_action"
        ]
      },
      "OpensInput": {
        "type": "object",
        "properties": {
          "max_opens": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 3,
            "maximum": 1000,
            "description": "Limit of issued passes (devices are counted). null — remove the limit."
          },
          "reset_counter": {
            "type": "boolean",
            "default": false,
            "description": "Reset the opens counter."
          }
        },
        "example": {
          "max_opens": 5,
          "reset_counter": true
        }
      },
      "SubscribersInput": {
        "type": "object",
        "properties": {
          "channel_ids": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid"
            },
            "minItems": 1,
            "maxItems": 3,
            "description": "Channels of the project owner (verified, same platform, no duplicates). Connected in the panel profile."
          }
        },
        "required": [
          "channel_ids"
        ]
      },
      "AccessEmail": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "email": {
            "type": "string"
          },
          "label": {
            "type": [
              "string",
              "null"
            ]
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "id",
          "email",
          "label",
          "created_at"
        ]
      },
      "AccessEmailWithStats": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "email": {
            "type": "string"
          },
          "label": {
            "type": [
              "string",
              "null"
            ]
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "devices": {
            "type": "integer",
            "description": "Number of devices (sessions) for the address."
          },
          "last_seen_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Last login."
          }
        },
        "required": [
          "id",
          "email",
          "label",
          "created_at",
          "devices",
          "last_seen_at"
        ]
      },
      "UnknownAttempt": {
        "type": "object",
        "properties": {
          "email": {
            "type": "string"
          },
          "attempts": {
            "type": "integer"
          },
          "last_at": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "email",
          "attempts",
          "last_at"
        ],
        "description": "Login attempt from an address not in the list."
      },
      "EmailListResponse": {
        "type": "object",
        "properties": {
          "emails": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AccessEmailWithStats"
            }
          },
          "limit": {
            "type": "integer",
            "description": "Address ceiling (1000)."
          },
          "warn_at": {
            "type": "integer",
            "description": "Warning threshold (950)."
          },
          "unknown_attempts": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/UnknownAttempt"
            },
            "description": "Last 20 attempts from unknown addresses."
          }
        },
        "required": [
          "emails",
          "limit",
          "warn_at",
          "unknown_attempts"
        ]
      },
      "AddEmailInput": {
        "type": "object",
        "properties": {
          "email": {
            "type": "string"
          },
          "label": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 60,
            "description": "Name for the log (optional)."
          }
        },
        "required": [
          "email"
        ],
        "example": {
          "email": "anna@example.ru",
          "label": "Anna"
        }
      },
      "BulkEmailsInput": {
        "type": "object",
        "properties": {
          "raw": {
            "type": "string",
            "description": "Addresses separated by newlines, commas, semicolons or spaces. Names are not supported. Up to 5000 tokens per call."
          }
        },
        "required": [
          "raw"
        ],
        "example": {
          "raw": "anna@example.ru, ivan@example.ru\npetr@example.ru"
        }
      },
      "BulkAddResult": {
        "type": "object",
        "properties": {
          "added": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AccessEmail"
            }
          },
          "duplicates": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Already in the list."
          },
          "invalid": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Do not look like addresses."
          },
          "overflow": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Did not fit within the 1000 ceiling."
          },
          "failed": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Not processed due to a database failure — retry."
          }
        },
        "required": [
          "added",
          "duplicates",
          "invalid",
          "overflow",
          "failed"
        ],
        "description": "Partial success is normal: check the breakdown."
      },
      "EmailLabelInput": {
        "type": "object",
        "properties": {
          "label": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 60,
            "description": "Empty/null — clear the name."
          }
        },
        "example": {
          "label": "Anna, photo shoot on July 12"
        }
      },
      "EmailLabelResponse": {
        "type": "object",
        "properties": {
          "email": {
            "$ref": "#/components/schemas/AccessEmail"
          }
        },
        "required": [
          "email"
        ]
      },
      "RemoveEmailsInput": {
        "type": "object",
        "properties": {
          "raw": {
            "type": "string",
            "default": "",
            "description": "Addresses to remove (same separators as in bulk). Required if all=false."
          },
          "all": {
            "type": "boolean",
            "default": false,
            "description": "true — clear the whole list."
          },
          "confirm": {
            "type": "boolean",
            "default": false,
            "description": "MUST be true — deliberate bulk removal (otherwise 400)."
          }
        },
        "required": [
          "confirm"
        ],
        "example": {
          "raw": "anna@example.ru",
          "confirm": true
        }
      },
      "BulkRemoveResult": {
        "type": "object",
        "properties": {
          "removed": {
            "type": "integer",
            "description": "Number removed."
          },
          "missing": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Addresses that were not in the list."
          }
        },
        "required": [
          "removed",
          "missing"
        ]
      },
      "MembersSettings": {
        "type": "object",
        "properties": {
          "ask_name": {
            "type": "boolean"
          },
          "name_label": {
            "type": "string",
            "enum": [
              "name",
              "fio"
            ]
          },
          "consent_mode": {
            "type": "string",
            "enum": [
              "button",
              "checkbox"
            ]
          },
          "moderation": {
            "type": "boolean",
            "description": "After the code, the member waits for the owner's approval."
          },
          "form_style": {
            "type": "string",
            "enum": [
              "single",
              "steps"
            ]
          },
          "max_seats": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Seat limit (null — no limit)."
          },
          "reconsent_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Consents given before this timestamp are requested again."
          }
        },
        "required": [
          "ask_name",
          "name_label",
          "consent_mode",
          "moderation",
          "form_style",
          "max_seats",
          "reconsent_at"
        ]
      },
      "MembersModeResponse": {
        "type": "object",
        "properties": {
          "access": {
            "$ref": "#/components/schemas/AccessState"
          },
          "members_settings": {
            "$ref": "#/components/schemas/MembersSettings"
          },
          "next_action": {
            "type": "string"
          }
        },
        "required": [
          "access",
          "members_settings",
          "next_action"
        ]
      },
      "Member": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "email": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "active",
              "rejected",
              "blocked"
            ],
            "description": "Member status (values match the `counts` counters; see code)."
          },
          "owner_note": {
            "type": "string"
          },
          "consents": {
            "type": [
              "array",
              "object",
              "null"
            ],
            "additionalProperties": true,
            "description": "Consent snapshot: list of `{title, url}` documents (see `members::build_consents`)."
          },
          "consented_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "paid_until": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "paid_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "last_login_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        },
        "required": [
          "id",
          "email",
          "name",
          "status",
          "owner_note",
          "consents",
          "consented_at",
          "paid_until",
          "paid_at",
          "created_at",
          "last_login_at"
        ],
        "description": "Site member in member registration mode (`MemberRow`; site_id and consent_ip are not exposed)."
      },
      "SiteDocument": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "position": {
            "type": "integer"
          },
          "title": {
            "type": "string"
          },
          "url": {
            "type": "string",
            "format": "uri"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "id",
          "position",
          "title",
          "url",
          "updated_at"
        ]
      },
      "MembersListResponse": {
        "type": "object",
        "properties": {
          "members": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Member"
            },
            "description": "Up to 500 members."
          },
          "counts": {
            "type": "object",
            "properties": {
              "pending": {
                "type": "integer"
              },
              "active": {
                "type": "integer"
              },
              "rejected": {
                "type": "integer"
              },
              "blocked": {
                "type": "integer"
              }
            },
            "required": [
              "pending",
              "active",
              "rejected",
              "blocked"
            ]
          },
          "settings": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/MembersSettings"
              },
              {
                "type": "null"
              }
            ],
            "description": "null if the site has no gate row."
          },
          "documents": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SiteDocument"
            }
          }
        },
        "required": [
          "members",
          "counts",
          "settings",
          "documents"
        ]
      },
      "ModerateInput": {
        "type": "object",
        "properties": {
          "action": {
            "type": "string",
            "enum": [
              "approve",
              "reject",
              "block",
              "unblock"
            ]
          }
        },
        "required": [
          "action"
        ],
        "example": {
          "action": "approve"
        }
      },
      "ModerateResponse": {
        "type": "object",
        "properties": {
          "member": {
            "$ref": "#/components/schemas/Member"
          }
        },
        "required": [
          "member"
        ]
      },
      "MembersSettingsInput": {
        "type": "object",
        "properties": {
          "ask_name": {
            "type": "boolean"
          },
          "name_label": {
            "type": "string",
            "enum": [
              "name",
              "fio"
            ]
          },
          "consent_mode": {
            "type": "string",
            "enum": [
              "button",
              "checkbox"
            ]
          },
          "moderation": {
            "type": "boolean"
          },
          "form_style": {
            "type": "string",
            "enum": [
              "single",
              "steps"
            ]
          },
          "max_seats": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 1,
            "maximum": 100000,
            "description": "null — remove the limit; absent — unchanged."
          }
        },
        "description": "Patch: absent field = unchanged.",
        "example": {
          "moderation": true,
          "max_seats": 50
        }
      },
      "DocumentInput": {
        "type": "object",
        "properties": {
          "title": {
            "type": "string",
            "maxLength": 60,
            "description": "No markup (`<`, `>` are forbidden)."
          },
          "url": {
            "type": "string",
            "maxLength": 512,
            "description": "https:// only, with a dot in the host, no spaces or quotes."
          }
        },
        "required": [
          "title",
          "url"
        ]
      },
      "DocumentsInput": {
        "type": "object",
        "properties": {
          "documents": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/DocumentInput"
            },
            "maxItems": 3,
            "description": "Full replacement of the set (0–3). Empty list — remove all."
          }
        },
        "required": [
          "documents"
        ],
        "example": {
          "documents": [
            {
              "title": "Terms of Service",
              "url": "https://example.ru/oferta"
            }
          ]
        }
      },
      "DocumentsResponse": {
        "type": "object",
        "properties": {
          "documents": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SiteDocument"
            }
          },
          "next_action": {
            "type": "string"
          }
        },
        "required": [
          "documents",
          "next_action"
        ]
      },
      "DayRow": {
        "type": "object",
        "properties": {
          "day": {
            "type": "string",
            "format": "date"
          },
          "requests": {
            "type": "integer"
          },
          "page_views": {
            "type": "integer"
          },
          "bytes": {
            "type": "integer"
          },
          "uniques": {
            "type": "integer"
          },
          "human_page_views": {
            "type": "integer"
          },
          "human_bytes": {
            "type": "integer"
          },
          "human_uniques": {
            "type": "integer"
          },
          "status_4xx": {
            "type": "integer"
          },
          "status_5xx": {
            "type": "integer"
          }
        },
        "required": [
          "day",
          "requests",
          "page_views",
          "bytes",
          "uniques",
          "human_page_views",
          "human_bytes",
          "human_uniques",
          "status_4xx",
          "status_5xx"
        ],
        "description": "Daily row (Moscow day). Days without traffic are zeros."
      },
      "DayCount": {
        "type": "object",
        "properties": {
          "day": {
            "type": "string",
            "format": "date"
          },
          "count": {
            "type": "integer"
          }
        },
        "required": [
          "day",
          "count"
        ]
      },
      "StatsTotals": {
        "type": "object",
        "properties": {
          "requests": {
            "type": "integer"
          },
          "page_views": {
            "type": "integer"
          },
          "human_page_views": {
            "type": "integer"
          },
          "bytes": {
            "type": "integer"
          },
          "human_bytes": {
            "type": "integer"
          },
          "visitors_daily_sum": {
            "type": "integer",
            "description": "Sum of daily uniques (cross-day uniqueness is impossible by construction)."
          },
          "human_visitors_daily_sum": {
            "type": "integer"
          },
          "status_4xx": {
            "type": "integer"
          },
          "status_5xx": {
            "type": "integer"
          }
        },
        "required": [
          "requests",
          "page_views",
          "human_page_views",
          "bytes",
          "human_bytes",
          "visitors_daily_sum",
          "human_visitors_daily_sum",
          "status_4xx",
          "status_5xx"
        ]
      },
      "StatsResponse": {
        "type": "object",
        "properties": {
          "from": {
            "type": "string",
            "format": "date"
          },
          "to": {
            "type": "string",
            "format": "date"
          },
          "totals": {
            "$ref": "#/components/schemas/StatsTotals"
          },
          "forms_total": {
            "type": "integer",
            "description": "Submissions for the period (excluding spam)."
          },
          "series": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/DayRow"
            }
          },
          "forms": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/DayCount"
            },
            "description": "Submissions per day."
          },
          "deploys": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/DayCount"
            },
            "description": "Deploys per day."
          },
          "note": {
            "type": "string"
          }
        },
        "required": [
          "from",
          "to",
          "totals",
          "forms_total",
          "series",
          "forms",
          "deploys",
          "note"
        ]
      },
      "TopRow": {
        "type": "object",
        "properties": {
          "kind": {
            "type": "string",
            "enum": [
              "page",
              "referrer",
              "download",
              "missing",
              "device",
              "filtered"
            ]
          },
          "key": {
            "type": "string"
          },
          "hits": {
            "type": "integer"
          }
        },
        "required": [
          "kind",
          "key",
          "hits"
        ]
      },
      "TopsResponse": {
        "type": "object",
        "properties": {
          "from": {
            "type": "string",
            "format": "date"
          },
          "to": {
            "type": "string",
            "format": "date"
          },
          "tops": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/TopRow"
            },
            "description": "Top 20 per kind: page, referrer, download, missing (404), device; rows with fewer than min_hits are hidden."
          },
          "filtered": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/TopRow"
            },
            "description": "Bot filter reasons (kind=filtered), no threshold."
          },
          "min_hits": {
            "type": "integer",
            "description": "Small-number suppression threshold (5)."
          },
          "note": {
            "type": "string"
          }
        },
        "required": [
          "from",
          "to",
          "tops",
          "filtered",
          "min_hits",
          "note"
        ]
      },
      "SiteGit": {
        "type": "object",
        "properties": {
          "site_id": {
            "type": "string",
            "format": "uuid"
          },
          "provider": {
            "type": "string",
            "description": "`generic` for connections made via REST."
          },
          "clone_url": {
            "type": "string"
          },
          "branch": {
            "type": "string"
          },
          "auth_user": {
            "type": [
              "string",
              "null"
            ]
          },
          "connection_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "install_command": {
            "type": [
              "string",
              "null"
            ]
          },
          "build_command": {
            "type": [
              "string",
              "null"
            ]
          },
          "output_dir": {
            "type": [
              "string",
              "null"
            ]
          },
          "root_dir": {
            "type": [
              "string",
              "null"
            ]
          },
          "auto_deploy": {
            "type": "boolean"
          },
          "preview_enabled": {
            "type": "boolean"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "site_id",
          "provider",
          "clone_url",
          "branch",
          "auth_user",
          "connection_id",
          "install_command",
          "build_command",
          "output_dir",
          "root_dir",
          "auto_deploy",
          "preview_enabled",
          "created_at",
          "updated_at"
        ],
        "description": "Site repository connection (`shared::model::SiteGit`; secrets are not returned)."
      },
      "GitConfigInput": {
        "type": "object",
        "properties": {
          "clone_url": {
            "type": "string",
            "format": "uri",
            "description": "https URL of the repository (required). https only, no userinfo; the host must resolve to public addresses (anti-SSRF), otherwise 400."
          },
          "branch": {
            "type": [
              "string",
              "null"
            ],
            "default": "main"
          },
          "auth_user": {
            "type": [
              "string",
              "null"
            ],
            "description": "User name for HTTP Basic (default oauth2). Not needed for a public repository."
          },
          "token": {
            "type": [
              "string",
              "null"
            ],
            "description": "PAT/token for a private repository. Encrypted on the server, never returned. Empty → public repository."
          },
          "install_command": {
            "type": [
              "string",
              "null"
            ]
          },
          "build_command": {
            "type": [
              "string",
              "null"
            ]
          },
          "output_dir": {
            "type": [
              "string",
              "null"
            ]
          },
          "root_dir": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "clone_url"
        ],
        "example": {
          "clone_url": "https://gitverse.ru/user/site.git",
          "branch": "main",
          "token": "glpat-…",
          "build_command": "npm run build",
          "output_dir": "dist"
        }
      },
      "GitDeployInput": {
        "type": "object",
        "properties": {
          "branch": {
            "type": [
              "string",
              "null"
            ],
            "description": "Override the branch (default — the live branch of the connection)."
          }
        },
        "example": {}
      },
      "WebhookResponse": {
        "type": "object",
        "properties": {
          "webhook_url": {
            "type": "string",
            "format": "uri"
          },
          "secret": {
            "type": "string",
            "description": "Webhook secret — shown ONCE."
          },
          "hint": {
            "type": "string",
            "description": "How to configure the webhook at the provider (GitVerse/GitLab/GitHub/Gitea)."
          }
        },
        "required": [
          "webhook_url",
          "secret",
          "hint"
        ],
        "example": {
          "webhook_url": "https://api.tuqo.ru/git/webhook/2b1d6a7e-0c1f-4b6e-9a2d-5f3c8e7d1a20",
          "secret": "0123…",
          "hint": "Add a webhook in the repository: URL = webhook_url, method POST, format application/json, event push …"
        }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "Invalid data: body/parameter validation failed (JSON error `{message}`). axum also returns 400 as text/plain for syntactically invalid JSON, an invalid UUID in the path or invalid query parameters.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "message": "site name is required"
            }
          }
        }
      },
      "Unauthorized": {
        "description": "Missing/invalid `Authorization: Bearer tqk_...` header, key revoked or expired. After 30 failed attempts from one IP within 60 s — 401 without checking the key.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "message": "Authorization: Bearer tqk_... header is required"
            }
          }
        }
      },
      "PaymentRequired": {
        "description": "The feature or limit requires another plan/option, or the project is suspended for non-payment.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "message": "build preview is available from the Start plan."
            }
          }
        }
      },
      "Forbidden": {
        "description": "The key lacks the required scope.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "message": "insufficient permissions"
            }
          }
        }
      },
      "NotFound": {
        "description": "Resource not found or belongs to another project (tenant isolation: resources of other projects are indistinguishable from missing ones).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "message": "not found"
            }
          }
        }
      },
      "Conflict": {
        "description": "State conflict (subdomain taken, storage/site/domain quota, invalid deploy status, etc.).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "message": "subdomain 'moe-portfolio' is already taken"
            }
          }
        }
      },
      "PayloadTooLarge": {
        "description": "Request body exceeds the route limit (axum, text/plain). Limits: JSON routes — 2 MB by default; deploys/deploy-files/blobs — 50 MB; autoreply/files — 52 MB.",
        "content": {
          "text/plain": {
            "schema": {
              "type": "string"
            }
          }
        }
      },
      "UnsupportedMediaType": {
        "description": "JSON route without `Content-Type: application/json` (axum, text/plain).",
        "content": {
          "text/plain": {
            "schema": {
              "type": "string"
            }
          }
        }
      },
      "UnprocessableEntity": {
        "description": "JSON is syntactically valid but does not match the schema: missing required field or wrong type (axum, text/plain).",
        "content": {
          "text/plain": {
            "schema": {
              "type": "string"
            }
          }
        }
      },
      "TooManyRequests": {
        "description": "Rate limit exceeded: deploys per day, build minutes per month, domain DNS checks (once per 10 s).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "message": "domain check was run recently, please wait"
            }
          }
        }
      },
      "InternalError": {
        "description": "Internal error (database, Redis, S3).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "message": "internal error"
            }
          }
        }
      },
      "Unavailable": {
        "description": "Service temporarily unavailable (for example, storage is not configured).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "message": "storage is not configured"
            }
          }
        }
      }
    }
  }
}