{
  "openapi": "3.1.0",
  "info": {
    "title": "Maketa API",
    "version": "1.1.0",
    "summary": "Публичный API конструктора экранов Maketa (доски, экраны, автономные ИИ-агенты)",
    "description": "Maketa — программируемый конструктор экранов приложений. Модель сцены — JSON `maketa.board.v1`.\n\nПуть автономного агента без человека: `POST /agent/register` → ключ `mk_sb_…` (заголовок `x-maketa-key`) → `POST /create` → `POST /save` → поделиться ссылкой `https://maketa.pro/app/?b=<id>` → `GET /load?id=<id>` читается любым (linkAccess=view по умолчанию).\n\nВсе ошибки — JSON `{error, hint?}`: `error` — стабильный машинный код, `hint` — по-русски, что делать. 410 Gone — доска удалена навсегда.\n\nПесочный ключ mk_sb_ живёт 14 дней и НЕ даёт ИИ-функции (/ai/*) и оплату. Постоянный доступ (проектный ключ mk_pk_) выдаёт владелец доски. MCP-endpoint: POST /mcp (OAuth 2.1, либо Bearer mk_pk_/mk_sb_). Документация: https://maketa.pro/docs/ и https://maketa.pro/llms.txt",
    "contact": { "name": "Студия demda.pro", "url": "https://demda.pro/" }
  },
  "servers": [{ "url": "https://maketa.pro/maketa/api" }],
  "tags": [
    { "name": "agent", "description": "Автономные ИИ-агенты: песочная регистрация и свои доски" },
    { "name": "boards", "description": "Доски (создание, сохранение, чтение)" },
    { "name": "screens", "description": "Экраны из кода и ветки" },
    { "name": "keys", "description": "Проектные ключи mk_pk_ (постоянный headless-доступ к одной доске)" },
    { "name": "misc", "description": "Служебное" }
  ],
  "paths": {
    "/agent/register": {
      "post": {
        "tags": ["agent"],
        "operationId": "agentRegister",
        "summary": "Регистрация автономного агента (без email и человека)",
        "description": "Анонимно выдаёт песочный ключ `mk_sb_…` на 14 дней. Ключ показывается ОДИН раз (на сервере хранится только хэш). Квоты: 5 досок, 200 сохранений в сутки. Лимит: 3 регистрации в сутки с IP.",
        "requestBody": {
          "required": false,
          "content": { "application/json": { "schema": {
            "type": "object",
            "properties": {
              "name": { "type": "string", "maxLength": 60, "description": "Имя агента" },
              "purpose": { "type": "string", "maxLength": 200, "description": "Зачем агенту доступ" }
            }
          }, "example": { "name": "claude-code", "purpose": "макет экрана для клиента" } } }
        },
        "responses": {
          "200": { "description": "Ключ выдан (сохраните — второй раз не показывается)", "content": { "application/json": { "schema": {
            "type": "object",
            "required": ["key", "expiresAt", "quotas", "docs"],
            "properties": {
              "key": { "type": "string", "description": "Песочный ключ mk_sb_… — передавайте в заголовке x-maketa-key" },
              "expiresAt": { "type": "integer", "description": "Unix ms, +14 дней" },
              "quotas": { "type": "object", "properties": { "boards": { "type": "integer" }, "savesPerDay": { "type": "integer" } } },
              "docs": { "type": "string" },
              "hint": { "type": "string" }
            }
          } } } },
          "429": { "$ref": "#/components/responses/TooMany" }
        }
      }
    },
    "/agent/boards": {
      "get": {
        "tags": ["agent"],
        "operationId": "agentBoards",
        "summary": "Доски песочного ключа со ссылками просмотра/редактирования",
        "security": [{ "maketaKey": [] }],
        "responses": {
          "200": { "description": "Список досок агента", "content": { "application/json": { "schema": {
            "type": "object",
            "properties": {
              "boards": { "type": "array", "items": { "type": "object", "properties": {
                "id": { "type": "string" }, "name": { "type": "string" }, "screens": { "type": "integer" },
                "updatedAt": { "type": "integer" }, "edit": { "type": "string" }, "view": { "type": "string" },
                "url": { "type": "string", "description": "Ссылка на доску: https://maketa.pro/app/?b=<id>" },
                "editUrl": { "type": "string" }, "viewUrl": { "type": "string" }
              } } },
              "expiresAt": { "type": "integer" },
              "quotas": { "type": "object" }
            }
          } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/create": {
      "post": {
        "tags": ["boards"],
        "operationId": "createBoard",
        "summary": "Создать доску",
        "description": "Анонимно или с ключом. Ответ содержит секретные токены `edit` (право правки) и `view`. С заголовком `x-maketa-key: mk_sb_…` доска привязывается к песочному ключу агента — дальше /save и /load работают по этому ключу без edit-токена. По умолчанию `linkAccess=view`: любой, знающий id, может читать доску (ссылка ?b=<id>).",
        "security": [{}, { "maketaKey": [] }],
        "requestBody": { "required": true, "content": { "application/json": { "schema": {
          "type": "object", "required": ["doc"],
          "properties": { "doc": { "$ref": "#/components/schemas/Doc" } }
        }, "example": { "doc": { "name": "Мой макет", "screens": [{ "id": "s_1", "name": "Главная", "device": "iphone-15", "w": 390, "h": 844, "bg": "#FFFFFF", "objects": [] }] } } } } },
        "responses": {
          "200": { "description": "Доска создана", "content": { "application/json": { "schema": {
            "type": "object", "required": ["id", "edit", "view"],
            "properties": { "id": { "type": "string" }, "edit": { "type": "string", "description": "Секретный edit-токен — сохраните" }, "view": { "type": "string" } }
          } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "description": "Квота досок песочного ключа исчерпана (quota_boards)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "429": { "$ref": "#/components/responses/TooMany" }
        }
      }
    },
    "/save": {
      "post": {
        "tags": ["boards"],
        "operationId": "saveBoard",
        "summary": "Сохранить сцену доски",
        "description": "Право на запись: edit-токен в поле `edit`, ИЛИ заголовок `x-maketa-key` с песочным ключом, которым доска создана, ИЛИ сессия владельца/соавтора, ИЛИ linkAccess=edit. Оптимистичная блокировка: передавайте `baseRev` из последнего /load|/save — при более новой ревизии на сервере придёт 409 conflict. Песочный ключ: 429 quota_saves при превышении 200 сохранений/сутки.",
        "security": [{}, { "maketaKey": [] }],
        "requestBody": { "required": true, "content": { "application/json": { "schema": {
          "type": "object", "required": ["id", "doc"],
          "properties": {
            "id": { "type": "string" },
            "edit": { "type": "string", "description": "edit-токен (не нужен при x-maketa-key своей доски)" },
            "doc": { "$ref": "#/components/schemas/Doc" },
            "baseRev": { "type": "integer", "description": "Ревизия, от которой правили (из /load)" }
          }
        } } } },
        "responses": {
          "200": { "description": "Сохранено", "content": { "application/json": { "schema": {
            "type": "object", "properties": { "ok": { "type": "boolean" }, "rev": { "type": "integer" }, "updatedAt": { "type": "integer" } }
          } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": { "description": "conflict — на сервере более новая ревизия", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "410": { "$ref": "#/components/responses/Gone" },
          "429": { "$ref": "#/components/responses/TooMany" }
        }
      }
    },
    "/load": {
      "get": {
        "tags": ["boards"],
        "operationId": "loadBoard",
        "summary": "Загрузить сцену доски",
        "description": "Доступ: публичный при linkAccess=edit|view (по умолчанию view), либо edit/view-токен в query, либо x-maketa-key своей доски (даёт access=edit), либо сессия. Ссылка для людей: https://maketa.pro/app/?b=<id>.",
        "security": [{}, { "maketaKey": [] }, { "editToken": [] }],
        "parameters": [
          { "name": "id", "in": "query", "required": true, "schema": { "type": "string" } },
          { "name": "edit", "in": "query", "required": false, "schema": { "type": "string" } },
          { "name": "view", "in": "query", "required": false, "schema": { "type": "string" } }
        ],
        "responses": {
          "200": { "description": "Сцена", "content": { "application/json": { "schema": {
            "type": "object",
            "properties": {
              "doc": { "$ref": "#/components/schemas/Doc" },
              "access": { "type": "string", "enum": ["edit", "view"] },
              "linkAccess": { "type": "string", "enum": ["edit", "view", "none"] },
              "rev": { "type": "integer" },
              "updatedAt": { "type": "integer" }
            }
          } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "410": { "$ref": "#/components/responses/Gone" }
        }
      }
    },
    "/rev": {
      "get": {
        "tags": ["boards"],
        "operationId": "boardRev",
        "summary": "Лёгкая сверка ревизии (для live-подтяжки правок)",
        "description": "Возвращает только {rev, updatedAt} — клиент опрашивает ~раз в 15 секунд и перезагружает doc через /load, когда rev вырос. Права чтения и ошибки — те же, что у /load.",
        "security": [{}, { "maketaKey": [] }, { "editToken": [] }],
        "parameters": [
          { "name": "id", "in": "query", "required": true, "schema": { "type": "string" } },
          { "name": "edit", "in": "query", "required": false, "schema": { "type": "string" } },
          { "name": "view", "in": "query", "required": false, "schema": { "type": "string" } }
        ],
        "responses": {
          "200": { "description": "Текущая ревизия", "content": { "application/json": { "schema": {
            "type": "object", "required": ["rev"], "properties": { "rev": { "type": "integer" }, "updatedAt": { "type": "integer" } }
          } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "410": { "$ref": "#/components/responses/Gone" }
        }
      }
    },
    "/screen/push": {
      "post": {
        "tags": ["screens"],
        "operationId": "pushCodeScreen",
        "summary": "Upsert code-экрана из кода (CI, maketa_sync)",
        "description": "Требует проектный ключ mk_pk_ этой доски в заголовке x-maketa-key (песочный mk_sb_ здесь не действует). Upsert по стабильному `key` (например, роуту), каждая загрузка — новая версия.",
        "security": [{ "maketaKey": [] }],
        "requestBody": { "required": true, "content": { "application/json": { "schema": {
          "type": "object", "required": ["id", "key", "screen"],
          "properties": {
            "id": { "type": "string" },
            "key": { "type": "string", "description": "Стабильный ключ экрана, 1–80 симв. [A-Za-z0-9_./-]" },
            "screen": { "$ref": "#/components/schemas/Screen" },
            "meta": { "type": "object", "properties": { "repo": { "type": "string" }, "route": { "type": "string" }, "commit": { "type": "string" }, "method": { "type": "string" } } }
          }
        } } } },
        "responses": {
          "200": { "description": "Экран синхронизирован", "content": { "application/json": { "schema": {
            "type": "object", "properties": { "ok": { "type": "boolean" }, "screenId": { "type": "string" }, "version": { "type": "integer" }, "screens": { "type": "integer" } }
          } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": { "description": "too_many_screens", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "410": { "$ref": "#/components/responses/Gone" },
          "429": { "$ref": "#/components/responses/TooMany" }
        }
      }
    },
    "/screen/branch": {
      "post": {
        "tags": ["screens"],
        "operationId": "branchScreen",
        "summary": "Создать редактируемую ветку экрана",
        "description": "Право: edit-токен (поле edit), сессия владельца, проектный ключ или песочный ключ своей доски (x-maketa-key). Для code-экранов ветка — единственный способ правки.",
        "security": [{}, { "maketaKey": [] }],
        "requestBody": { "required": true, "content": { "application/json": { "schema": {
          "type": "object", "required": ["id", "screenId"],
          "properties": { "id": { "type": "string" }, "screenId": { "type": "string" }, "edit": { "type": "string" }, "name": { "type": "string" } }
        } } } },
        "responses": {
          "200": { "description": "Ветка создана", "content": { "application/json": { "schema": {
            "type": "object", "properties": { "ok": { "type": "boolean" }, "screenId": { "type": "string" }, "parentId": { "type": "string" } }
          } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": { "description": "too_many_screens", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "410": { "$ref": "#/components/responses/Gone" }
        }
      }
    },
    "/project/key": {
      "post": {
        "tags": ["keys"],
        "operationId": "createProjectKey",
        "summary": "Выдать проектный ключ mk_pk_ (показывается один раз)",
        "description": "Право: сессия владельца доски или edit-токен (поле edit). Ключ даёт headless-доступ (push экранов из кода, MCP) в рамках одной доски и не истекает, пока не отозван.",
        "requestBody": { "required": true, "content": { "application/json": { "schema": {
          "type": "object", "required": ["id"],
          "properties": { "id": { "type": "string" }, "edit": { "type": "string" }, "label": { "type": "string", "maxLength": 60 } }
        } } } },
        "responses": {
          "200": { "description": "Ключ выдан", "content": { "application/json": { "schema": {
            "type": "object", "properties": { "key": { "type": "string" }, "label": { "type": "string" } }
          } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": { "description": "too_many_keys", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "410": { "$ref": "#/components/responses/Gone" },
          "429": { "$ref": "#/components/responses/TooMany" }
        }
      }
    },
    "/project/keys": {
      "get": {
        "tags": ["keys"],
        "operationId": "listProjectKeys",
        "summary": "Список проектных ключей доски (без секретов)",
        "parameters": [
          { "name": "id", "in": "query", "required": true, "schema": { "type": "string" } },
          { "name": "edit", "in": "query", "required": false, "schema": { "type": "string" } }
        ],
        "responses": {
          "200": { "description": "Ключи", "content": { "application/json": { "schema": {
            "type": "object", "properties": { "keys": { "type": "array", "items": { "type": "object", "properties": {
              "label": { "type": "string" }, "hint": { "type": "string" }, "createdAt": { "type": "integer" }, "lastUsed": { "type": "integer" }
            } } } }
          } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "410": { "$ref": "#/components/responses/Gone" }
        }
      }
    },
    "/project/key/revoke": {
      "post": {
        "tags": ["keys"],
        "operationId": "revokeProjectKey",
        "summary": "Отозвать проектный ключ по последним 4 символам",
        "requestBody": { "required": true, "content": { "application/json": { "schema": {
          "type": "object", "required": ["id", "hint"],
          "properties": { "id": { "type": "string" }, "edit": { "type": "string" }, "hint": { "type": "string", "description": "последние 4 символа ключа" } }
        } } } },
        "responses": {
          "200": { "description": "Отозвано", "content": { "application/json": { "schema": { "type": "object", "properties": { "revoked": { "type": "integer" } } } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "410": { "$ref": "#/components/responses/Gone" }
        }
      }
    },
    "/health": {
      "get": {
        "tags": ["misc"],
        "operationId": "health",
        "summary": "Проверка живости сервиса",
        "responses": { "200": { "description": "ok", "content": { "application/json": { "schema": { "type": "object", "properties": { "ok": { "type": "boolean" }, "ts": { "type": "integer" } } } } } } }
      }
    },
    "/mcp": {
      "post": {
        "tags": ["misc"],
        "operationId": "mcp",
        "summary": "MCP-endpoint (JSON-RPC 2.0, Streamable HTTP)",
        "description": "Model Context Protocol для ИИ. Авторизация Bearer: OAuth 2.1-токен (полный per-user доступ, discovery /.well-known/oauth-authorization-server), проектный ключ mk_pk_ (headless, рамки своей доски — boardId обязателен в аргументах) или песочный ключ mk_sb_ (headless, доски песочницы). Ключи принимаются и в заголовке x-maketa-key. Начните с tools/list; инструменты maketa_*.",
        "security": [{ "mcpBearer": [] }, { "maketaKey": [] }],
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "description": "JSON-RPC 2.0 запрос или батч" } } } },
        "responses": {
          "200": { "description": "JSON-RPC ответ" },
          "401": { "description": "invalid_token", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "maketaKey": {
        "type": "apiKey", "in": "header", "name": "x-maketa-key",
        "description": "Песочный ключ агента mk_sb_… (POST /agent/register) или проектный ключ доски mk_pk_… (POST /project/key)."
      },
      "editToken": {
        "type": "apiKey", "in": "query", "name": "edit",
        "description": "Секретный edit-токен доски из ответа POST /create (в POST-ручках передаётся полем edit в теле)."
      },
      "mcpBearer": {
        "type": "http", "scheme": "bearer",
        "description": "OAuth 2.1 access-токен, либо непосредственно ключ mk_pk_/mk_sb_."
      }
    },
    "responses": {
      "BadRequest": { "description": "Некорректный запрос", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "Unauthorized": { "description": "Нет или неверный/просроченный ключ (bad_key | key_expired | no_key). hint подскажет, как получить новый.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "Forbidden": { "description": "Прав нет (forbidden | quota_boards)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "NotFound": { "description": "Не существует (not_found)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "Gone": { "description": "Удалено навсегда (gone) — с deletedAt и reason", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/GoneError" } } } },
      "TooMany": { "description": "Rate limit / квота (too_many | quota_saves); retryAfterMin — минут до сброса окна", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": ["error"],
        "properties": {
          "error": { "type": "string", "description": "Стабильный машинный код: bad_id, bad_doc, bad_key, key_expired, no_key, forbidden, not_found, gone, conflict, quota_boards, quota_saves, too_many, too_many_keys, too_many_screens…" },
          "hint": { "type": "string", "description": "Человеко- и агенто-читаемая подсказка по-русски: что сделать, чтобы запрос удался" },
          "retryAfterMin": { "type": "integer", "description": "Для 429: через сколько минут окно лимита сбросится" }
        },
        "additionalProperties": true
      },
      "GoneError": {
        "allOf": [
          { "$ref": "#/components/schemas/Error" },
          { "type": "object", "properties": {
            "deletedAt": { "type": ["integer", "null"], "description": "Unix ms удаления" },
            "reason": { "type": "string", "description": "Например sandbox_expired — доска песочницы удалена спустя 30 дней после истечения ключа" }
          } }
        ]
      },
      "Doc": {
        "type": "object",
        "required": ["screens"],
        "description": "Сцена maketa.board.v1. Полное описание модели: https://maketa.pro/docs/#model",
        "properties": {
          "name": { "type": "string", "description": "Имя макета (показывается в списках досок)" },
          "screens": { "type": "array", "minItems": 1, "maxItems": 200, "items": { "$ref": "#/components/schemas/Screen" } }
        },
        "additionalProperties": true
      },
      "Screen": {
        "type": "object",
        "description": "Экран. kind: design (правится) | code (из кода, заблокирован — правки через ветку).",
        "properties": {
          "id": { "type": "string", "description": "1–40 симв. [A-Za-z0-9_-]" },
          "name": { "type": "string" },
          "device": { "type": "string", "description": "например iphone-15" },
          "w": { "type": "number", "default": 390 },
          "h": { "type": "number", "default": 844 },
          "bg": { "type": "string", "default": "#FFFFFF" },
          "kind": { "type": "string", "enum": ["design", "code"] },
          "objects": { "type": "array", "maxItems": 4000, "items": { "$ref": "#/components/schemas/BoardObject" } }
        },
        "additionalProperties": true
      },
      "BoardObject": {
        "type": "object",
        "required": ["type"],
        "description": "Объект board.v1. Общие поля: id, x, y (абсолютные px), rot (градусы), opacity 0–1, group (id логического блока), locked, link (id экрана-цели перехода прототипа). Типы: rect {w,h,fill:#hex|none,stroke,strokeWidth 0–12,radius:число|[tl,tr,br,bl]}; ellipse {w,h,fill,stroke,strokeWidth}; text {text≤5000,fontSize 8–80,weight 400|500|600|700,color,align,lineHeight 1–2,w:0=авторазмер}; path {points:[[x,y],…] относительно x,y, color, size 1–20}; image {w,h,src:data:image/…,radius}; note {text,n,color — аннотация}; tabs {w,h,bg,color,muted,active,items:[{text,icon,link}×2–5]} — нижний таб-бар одним объектом.",
        "properties": {
          "id": { "type": "string" },
          "type": { "type": "string", "enum": ["rect", "ellipse", "text", "path", "image", "note", "tabs"] },
          "x": { "type": "number" },
          "y": { "type": "number" }
        },
        "additionalProperties": true
      }
    }
  }
}
