openapi: 3.1.0
info:
  title: Coverse API
  version: "1.0.0"
  description: >
    Публичный REST API для чтения и записи данных в таблицах Coverse.


    API рассчитан на интеграции (машина-машина): создайте токен доступа в
    настройках аккаунта Coverse (раздел «Автоматизация и API») и обращайтесь к
    эндпоинтам `/v1`. Значения читаются из заранее вычисленного снимка, поэтому
    при чтении формулы уже посчитаны; запись проходит через движок таблиц, так
    что отправленные формулы пересчитываются на сервере.


    Как устроены значения ячеек (важно при построении интеграций):

    - Каждая ячейка возвращается как `{ v, m, f? }`. `v` — исходное значение,
      `m` — строка в том виде, в каком её видит пользователь, `f` — формула
      (только у формульных ячеек). Пустая ячейка возвращается как `null`.

    - Числа в `v` используют точку как десятичный разделитель. Строка `m`
      следует локали документа (в русском документе число может выглядеть как
      `1 234,50`).

    - Даты хранятся в `v` как серийные номера (число дней от эпохи таблицы), а в
      `m` — как отформатированная строка.

    - При записи формул используйте английские имена функций, `;` как разделитель
      аргументов и `,` как десятичный разделитель, например `=SUM(A1;B1)`,
      `=ROUND(3,14159;2)`.
  contact:
    name: Поддержка Coverse
    email: contact@coverse.team

servers:
  - url: https://app.coverse.team/v1
    description: Продакшн

security:
  - bearerAuth: []

x-tagGroups:
  - name: Справочник API
    tags: [Документы, Значения, Листы, Вебхуки, Аудит]
  - name: Общее
    tags: [Ошибки]

tags:
  - name: Ошибки
    description: >
      ## Ошибки


      При ошибке возвращается соответствующий HTTP-код и тело вида
      `{ "message": "..." }` с пояснением.


      | Код | Когда возникает |

      | --- | --- |

      | `401` | Токен доступа отсутствует или недействителен. |

      | `403` | У токена нет нужного права (scope), либо действие не разрешено тарифом (например, аудит — только на тарифе Профессиональный и выше). |

      | `404` | Документ или лист не существует или недоступен вашему токену. |

      | `422` | Некорректный запрос: неверный диапазон A1, документ не является таблицей, неправильное тело запроса; либо достигнут лимит вебхуков тарифа. |

      | `429` | Превышен лимит запросов. Базовый лимит — 60 запросов в минуту на токен; тарифы могут его повышать. |


      При `batchGet` некорректный диапазон или отсутствующий лист не роняет весь
      запрос — в ответе для такого диапазона приходит поле `error`, а остальные
      диапазоны отдаются нормально.
  - name: Документы
    description: Список документов и доступ к отдельному документу.
  - name: Значения
    description: Чтение и запись значений ячеек в таблице.
  - name: Листы
    description: Список и управление листами (вкладками) внутри таблицы.
  - name: Вебхуки
    description: Подписка на уведомления об изменениях документа.
  - name: Аудит
    description: Журнал изменений, сделанных через API.

paths:
  /documents:
    get:
      tags: [Документы]
      summary: Список документов
      description: >
        Возвращает документы, доступные владельцу токена. Требуется право
        `sheets.read`. В список попадает каждый документ, который пользователь
        может открыть; доступ проверяется по каждому документу теми же правами,
        что и в приложении.
      operationId: listDocuments
      x-codeSamples:
        - lang: cURL
          source: |
            curl "https://app.coverse.team/v1/documents?type=table" \
              -H "Authorization: Bearer $TOKEN"
        - lang: JavaScript
          source: |
            const res = await fetch("https://app.coverse.team/v1/documents?type=table", {
              headers: { Authorization: `Bearer ${token}` },
            });
            const data = await res.json();
        - lang: Python
          source: |
            import requests
            r = requests.get(
                "https://app.coverse.team/v1/documents",
                params={"type": "table"},
                headers={"Authorization": f"Bearer {token}"},
            )
            print(r.json())
      parameters:
        - name: type
          in: query
          required: false
          description: Фильтр по типу документа, например `table`.
          schema:
            type: string
            examples: [table]
      responses:
        "200":
          description: Список документов.
          content:
            application/json:
              schema:
                type: object
                properties:
                  documents:
                    type: array
                    items:
                      $ref: "#/components/schemas/Document"
              example:
                documents:
                  - id: 3f2b9c10-8a4e-4f21-9b7d-2c5e6a1f0d34
                    type: table
                    title: Заявки
                    revisionId: "121"
                    createdAt: "2026-06-01T10:00:00+00:00"
                    updatedAt: "2026-07-09T09:15:14+00:00"
                    content:
                      contentUrl: https://storage.coverse.team/…/content.json
                      stylesUrl: https://storage.coverse.team/…/styles.json
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"

  /documents/{document}:
    parameters:
      - $ref: "#/components/parameters/DocumentId"
    get:
      tags: [Документы]
      summary: Получить документ
      description: >
        Возвращает один документ по идентификатору. Требуется право
        `sheets.read` и доступ на просмотр документа.
      operationId: getDocument
      x-codeSamples:
        - lang: cURL
          source: |
            curl "https://app.coverse.team/v1/documents/$DOCUMENT_ID" \
              -H "Authorization: Bearer $TOKEN"
        - lang: JavaScript
          source: |
            const res = await fetch(`https://app.coverse.team/v1/documents/${documentId}`, {
              headers: { Authorization: `Bearer ${token}` },
            });
            const data = await res.json();
        - lang: Python
          source: |
            import requests
            r = requests.get(
                f"https://app.coverse.team/v1/documents/{document_id}",
                headers={"Authorization": f"Bearer {token}"},
            )
            print(r.json())
      responses:
        "200":
          description: Документ.
          content:
            application/json:
              schema:
                type: object
                properties:
                  document:
                    $ref: "#/components/schemas/Document"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"

  /documents/{document}/values/{range}:
    parameters:
      - $ref: "#/components/parameters/DocumentId"
      - $ref: "#/components/parameters/Range"
    get:
      tags: [Значения]
      summary: Прочитать диапазон
      description: >
        Читает значения из одного диапазона в A1-нотации. Требуется право
        `sheets.read`.


        Сегмент пути `{range}` может содержать имя листа и разделитель диапазона
        `:`, поэтому его нужно URL-кодировать (например `Sheet1!A1:C10`
        превращается в `Sheet1%21A1%3AC10`). Поддерживаются открытые диапазоны:
        `A:A` (весь столбец), `1:1` (вся строка), `A1:A` (открытый снизу).
        Результат разбивается на страницы и ограничен серверным лимитом строк.
      operationId: readRange
      x-codeSamples:
        - lang: cURL
          source: |
            curl "https://app.coverse.team/v1/documents/$DOCUMENT_ID/values/A1:B10" \
              -H "Authorization: Bearer $TOKEN"
        - lang: JavaScript
          source: |
            const range = encodeURIComponent("A1:B10");
            const res = await fetch(
              `https://app.coverse.team/v1/documents/${documentId}/values/${range}`,
              { headers: { Authorization: `Bearer ${token}` } },
            );
            const data = await res.json();
        - lang: Python
          source: |
            import requests
            from urllib.parse import quote
            r = requests.get(
                f"https://app.coverse.team/v1/documents/{document_id}/values/{quote('A1:B10')}",
                headers={"Authorization": f"Bearer {token}"},
            )
            print(r.json())
      parameters:
        - name: offset
          in: query
          required: false
          description: Смещение по строкам внутри разрешённого диапазона (от 0).
          schema:
            type: integer
            minimum: 0
            default: 0
        - name: limit
          in: query
          required: false
          description: >
            Максимальное число строк в ответе. Ограничивается серверным
            максимумом (1000). Если не указан, сервер возвращает до максимума и
            выставляет `hasMore`, когда есть ещё строки.
          schema:
            type: integer
            minimum: 1
      responses:
        "200":
          description: Значения диапазона.
          content:
            application/json:
              schema:
                type: object
                properties:
                  values:
                    $ref: "#/components/schemas/ValueRange"
              example:
                values:
                  sheet: Лист 1
                  range: A1:B2
                  majorDimension: ROWS
                  cells:
                    - - v: Имя
                        m: Имя
                      - v: Сумма
                        m: Сумма
                    - - v: Заявка №1
                        m: Заявка №1
                      - v: 3456
                        m: "3 456"
                  pagination:
                    offset: 0
                    limit: 1000
                    total: 2
                    returned: 2
                    hasMore: false
                    nextOffset: null
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
    put:
      tags: [Значения]
      summary: Записать диапазон
      description: >
        Записывает блок значений, где верхняя-левая ячейка диапазона —
        точка привязки. Требуется право `sheets.write` и доступ на
        редактирование. При `USER_ENTERED` (по умолчанию) ведущий `=` считается
        формулой и движок пересчитывает; при `RAW` всё записывается как есть.
      operationId: writeRange
      x-codeSamples:
        - lang: cURL
          source: |
            curl -X PUT "https://app.coverse.team/v1/documents/$DOCUMENT_ID/values/A1" \
              -H "Authorization: Bearer $TOKEN" \
              -H "Content-Type: application/json" \
              -d '{"values":[["Привет","=1+1"]],"valueInputOption":"USER_ENTERED"}'
        - lang: JavaScript
          source: |
            const res = await fetch(
              `https://app.coverse.team/v1/documents/${documentId}/values/A1`,
              {
                method: "PUT",
                headers: {
                  Authorization: `Bearer ${token}`,
                  "Content-Type": "application/json",
                },
                body: JSON.stringify({
                  values: [["Привет", "=1+1"]],
                  valueInputOption: "USER_ENTERED",
                }),
              },
            );
            const data = await res.json();
        - lang: Python
          source: |
            import requests
            r = requests.put(
                f"https://app.coverse.team/v1/documents/{document_id}/values/A1",
                headers={"Authorization": f"Bearer {token}"},
                json={"values": [["Привет", "=1+1"]], "valueInputOption": "USER_ENTERED"},
            )
            print(r.json())
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [values]
              properties:
                values:
                  $ref: "#/components/schemas/ValueMatrix"
                valueInputOption:
                  $ref: "#/components/schemas/ValueInputOption"
            example:
              values:
                - [Заявка №2, "=B2+100"]
              valueInputOption: USER_ENTERED
      responses:
        "200":
          description: Записанные ячейки.
          content:
            application/json:
              schema:
                type: object
                properties:
                  updated:
                    type: object
                    properties:
                      sheet:
                        type: [string, "null"]
                      cells:
                        type: integer
                        description: Число изменённых ячеек.
              example:
                updated:
                  sheet: Лист 1
                  cells: 2
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"

  /documents/{document}/values/batchGet:
    parameters:
      - $ref: "#/components/parameters/DocumentId"
    get:
      tags: [Значения]
      summary: Прочитать несколько диапазонов
      description: >
        Читает несколько диапазонов A1 за один запрос. Требуется право
        `sheets.read`. Некорректный диапазон или отсутствующий лист даёт запись с
        полем `error`, а не роняет весь запрос. Не более 20 диапазонов.
      operationId: batchGetValues
      x-codeSamples:
        - lang: cURL
          source: |
            curl "https://app.coverse.team/v1/documents/$DOCUMENT_ID/values/batchGet?ranges[]=A1:B2&ranges[]=D1:D5" \
              -H "Authorization: Bearer $TOKEN"
        - lang: JavaScript
          source: |
            const params = new URLSearchParams();
            ["A1:B2", "D1:D5"].forEach((r) => params.append("ranges[]", r));
            const res = await fetch(
              `https://app.coverse.team/v1/documents/${documentId}/values/batchGet?${params}`,
              { headers: { Authorization: `Bearer ${token}` } },
            );
            const data = await res.json();
        - lang: Python
          source: |
            import requests
            r = requests.get(
                f"https://app.coverse.team/v1/documents/{document_id}/values/batchGet",
                params={"ranges[]": ["A1:B2", "D1:D5"]},
                headers={"Authorization": f"Bearer {token}"},
            )
            print(r.json())
      parameters:
        - name: ranges
          in: query
          required: true
          description: По одной записи на диапазон, например `ranges[]=A1:B2&ranges[]=Sheet2!C1:C10`.
          style: form
          explode: true
          schema:
            type: array
            minItems: 1
            maxItems: 20
            items:
              type: string
      responses:
        "200":
          description: По одному диапазону значений на каждый запрошенный.
          content:
            application/json:
              schema:
                type: object
                properties:
                  valueRanges:
                    type: array
                    items:
                      oneOf:
                        - $ref: "#/components/schemas/ValueRange"
                        - type: object
                          properties:
                            range:
                              type: string
                            error:
                              type: string
              example:
                valueRanges:
                  - sheet: Лист 1
                    range: A1:A2
                    majorDimension: ROWS
                    cells:
                      - - v: Имя
                          m: Имя
                      - - v: Заявка №1
                          m: Заявка №1
                    pagination:
                      offset: 0
                      limit: 1000
                      total: 2
                      returned: 2
                      hasMore: false
                      nextOffset: null
                  - range: Лист 5!A1
                    error: Sheet not found
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"

  /documents/{document}/values/batchUpdate:
    parameters:
      - $ref: "#/components/parameters/DocumentId"
    post:
      tags: [Значения]
      summary: Записать несколько диапазонов атомарно
      description: >
        Записывает несколько диапазонов одной атомарной операцией. Требуется
        право `sheets.write`. Все диапазоны применяются за один прогон движка и
        одну транзакцию; если хотя бы один диапазон некорректен — весь запрос
        отклоняется и ничего не записывается.
      operationId: batchUpdateValues
      x-codeSamples:
        - lang: cURL
          source: |
            curl -X POST "https://app.coverse.team/v1/documents/$DOCUMENT_ID/values/batchUpdate" \
              -H "Authorization: Bearer $TOKEN" \
              -H "Content-Type: application/json" \
              -d '{"data":[{"range":"Лист 1!A2","values":[["Заявка №1",3456]]},{"range":"Лист 1!D1","values":[["Статус"]]}],"valueInputOption":"USER_ENTERED"}'
        - lang: JavaScript
          source: |
            const res = await fetch(
              `https://app.coverse.team/v1/documents/${documentId}/values/batchUpdate`,
              {
                method: "POST",
                headers: {
                  Authorization: `Bearer ${token}`,
                  "Content-Type": "application/json",
                },
                body: JSON.stringify({
                  data: [
                    { range: "Лист 1!A2", values: [["Заявка №1", 3456]] },
                    { range: "Лист 1!D1", values: [["Статус"]] },
                  ],
                  valueInputOption: "USER_ENTERED",
                }),
              },
            );
            const data = await res.json();
        - lang: Python
          source: |
            import requests
            r = requests.post(
                f"https://app.coverse.team/v1/documents/{document_id}/values/batchUpdate",
                headers={"Authorization": f"Bearer {token}"},
                json={
                    "data": [
                        {"range": "Лист 1!A2", "values": [["Заявка №1", 3456]]},
                        {"range": "Лист 1!D1", "values": [["Статус"]]},
                    ],
                    "valueInputOption": "USER_ENTERED",
                },
            )
            print(r.json())
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [data]
              properties:
                data:
                  type: array
                  minItems: 1
                  items:
                    type: object
                    required: [range, values]
                    properties:
                      range:
                        type: string
                      values:
                        $ref: "#/components/schemas/ValueMatrix"
                      valueInputOption:
                        $ref: "#/components/schemas/ValueInputOption"
                valueInputOption:
                  $ref: "#/components/schemas/ValueInputOption"
            example:
              data:
                - range: Лист 1!A2
                  values:
                    - [Заявка №1, 3456]
                - range: Лист 1!D1
                  values:
                    - [Статус]
              valueInputOption: USER_ENTERED
      responses:
        "200":
          description: Записанные ячейки.
          content:
            application/json:
              schema:
                type: object
                properties:
                  updated:
                    type: object
                    properties:
                      ranges:
                        type: array
                        items:
                          type: string
                      cells:
                        type: integer
              example:
                updated:
                  ranges: [Лист 1!A2:B2, Лист 1!D1]
                  cells: 3
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"

  /documents/{document}/values/append:
    parameters:
      - $ref: "#/components/parameters/DocumentId"
    post:
      tags: [Значения]
      summary: Дописать строки
      description: >
        Добавляет строки после последней заполненной строки листа. Требуется
        право `sheets.write`. Начальный столбец берётся из `range`; начальная
        строка — последняя заполненная плюс один.
      operationId: appendValues
      x-codeSamples:
        - lang: cURL
          source: |
            curl -X POST "https://app.coverse.team/v1/documents/$DOCUMENT_ID/values/append" \
              -H "Authorization: Bearer $TOKEN" \
              -H "Content-Type: application/json" \
              -d '{"range":"Лист 1!A1","values":[["Заявка №3",780]]}'
        - lang: JavaScript
          source: |
            const res = await fetch(
              `https://app.coverse.team/v1/documents/${documentId}/values/append`,
              {
                method: "POST",
                headers: {
                  Authorization: `Bearer ${token}`,
                  "Content-Type": "application/json",
                },
                body: JSON.stringify({ range: "Лист 1!A1", values: [["Заявка №3", 780]] }),
              },
            );
            const data = await res.json();
        - lang: Python
          source: |
            import requests
            r = requests.post(
                f"https://app.coverse.team/v1/documents/{document_id}/values/append",
                headers={"Authorization": f"Bearer {token}"},
                json={"range": "Лист 1!A1", "values": [["Заявка №3", 780]]},
            )
            print(r.json())
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [range, values]
              properties:
                range:
                  type: string
                values:
                  $ref: "#/components/schemas/ValueMatrix"
                valueInputOption:
                  $ref: "#/components/schemas/ValueInputOption"
            example:
              range: Лист 1!A1
              values:
                - [Заявка №3, 780]
                - [Заявка №4, 1290]
      responses:
        "200":
          description: Строки добавлены.
          content:
            application/json:
              schema:
                type: object
                properties:
                  updated:
                    type: object
                    properties:
                      sheet:
                        type: [string, "null"]
                      startRow:
                        type: integer
                        description: Строка (от 1), с которой начинается добавленный блок.
                      cells:
                        type: integer
              example:
                updated:
                  sheet: Лист 1
                  startRow: 6
                  cells: 4
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"

  /documents/{document}/values/clear:
    parameters:
      - $ref: "#/components/parameters/DocumentId"
    post:
      tags: [Значения]
      summary: Очистить диапазон
      description: >
        Очищает заполненные ячейки в диапазоне. Требуется право `sheets.write`.
        Удаляются только заполненные ячейки; открытые диапазоны разрешаются по
        размеру листа.
      operationId: clearValues
      x-codeSamples:
        - lang: cURL
          source: |
            curl -X POST "https://app.coverse.team/v1/documents/$DOCUMENT_ID/values/clear" \
              -H "Authorization: Bearer $TOKEN" \
              -H "Content-Type: application/json" \
              -d '{"range":"Лист 1!A2:B100"}'
        - lang: JavaScript
          source: |
            const res = await fetch(
              `https://app.coverse.team/v1/documents/${documentId}/values/clear`,
              {
                method: "POST",
                headers: {
                  Authorization: `Bearer ${token}`,
                  "Content-Type": "application/json",
                },
                body: JSON.stringify({ range: "Лист 1!A2:B100" }),
              },
            );
            const data = await res.json();
        - lang: Python
          source: |
            import requests
            r = requests.post(
                f"https://app.coverse.team/v1/documents/{document_id}/values/clear",
                headers={"Authorization": f"Bearer {token}"},
                json={"range": "Лист 1!A2:B100"},
            )
            print(r.json())
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [range]
              properties:
                range:
                  type: string
            example:
              range: Лист 1!A2:B100
      responses:
        "200":
          description: Ячейки очищены.
          content:
            application/json:
              schema:
                type: object
                properties:
                  updated:
                    type: object
                    properties:
                      sheet:
                        type: [string, "null"]
                      cells:
                        type: integer
              example:
                updated:
                  sheet: Лист 1
                  cells: 8
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"

  /documents/{document}/sheets:
    parameters:
      - $ref: "#/components/parameters/DocumentId"
    get:
      tags: [Листы]
      summary: Список листов
      description: Возвращает листы (вкладки) таблицы. Требуется право `sheets.read`.
      operationId: listSheets
      x-codeSamples:
        - lang: cURL
          source: |
            curl "https://app.coverse.team/v1/documents/$DOCUMENT_ID/sheets" \
              -H "Authorization: Bearer $TOKEN"
        - lang: JavaScript
          source: |
            const res = await fetch(
              `https://app.coverse.team/v1/documents/${documentId}/sheets`,
              { headers: { Authorization: `Bearer ${token}` } },
            );
            const data = await res.json();
        - lang: Python
          source: |
            import requests
            r = requests.get(
                f"https://app.coverse.team/v1/documents/{document_id}/sheets",
                headers={"Authorization": f"Bearer {token}"},
            )
            print(r.json())
      responses:
        "200":
          description: Листы.
          content:
            application/json:
              schema:
                type: object
                properties:
                  sheets:
                    type: array
                    items:
                      $ref: "#/components/schemas/Sheet"
              example:
                sheets:
                  - sheetId: "0"
                    uid: 8a4e4f21-9b7d-2c5e-6a1f-0d343f2b9c10
                    name: Лист 1
                    hidden: false
                    order: 0
                    color: null
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
    post:
      tags: [Листы]
      summary: Создать лист
      description: >
        Создаёт новый лист. Требуется право `sheets.write`. Имя необязательно;
        если не указано — назначается имя по умолчанию.
      operationId: createSheet
      x-codeSamples:
        - lang: cURL
          source: |
            curl -X POST "https://app.coverse.team/v1/documents/$DOCUMENT_ID/sheets" \
              -H "Authorization: Bearer $TOKEN" \
              -H "Content-Type: application/json" \
              -d '{"name":"Лист 2"}'
        - lang: JavaScript
          source: |
            const res = await fetch(
              `https://app.coverse.team/v1/documents/${documentId}/sheets`,
              {
                method: "POST",
                headers: {
                  Authorization: `Bearer ${token}`,
                  "Content-Type": "application/json",
                },
                body: JSON.stringify({ name: "Лист 2" }),
              },
            );
            const data = await res.json();
        - lang: Python
          source: |
            import requests
            r = requests.post(
                f"https://app.coverse.team/v1/documents/{document_id}/sheets",
                headers={"Authorization": f"Bearer {token}"},
                json={"name": "Лист 2"},
            )
            print(r.json())
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                  maxLength: 255
      responses:
        "201":
          description: Созданный лист.
          content:
            application/json:
              schema:
                type: object
                properties:
                  sheet:
                    $ref: "#/components/schemas/Sheet"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"

  /documents/{document}/sheets/{sheet}:
    parameters:
      - $ref: "#/components/parameters/DocumentId"
      - $ref: "#/components/parameters/SheetId"
    patch:
      tags: [Листы]
      summary: Переименовать лист
      description: Переименовывает лист. Требуется право `sheets.write`.
      operationId: renameSheet
      x-codeSamples:
        - lang: cURL
          source: |
            curl -X PATCH "https://app.coverse.team/v1/documents/$DOCUMENT_ID/sheets/$SHEET_ID" \
              -H "Authorization: Bearer $TOKEN" \
              -H "Content-Type: application/json" \
              -d '{"name":"Архив"}'
        - lang: JavaScript
          source: |
            const res = await fetch(
              `https://app.coverse.team/v1/documents/${documentId}/sheets/${sheetId}`,
              {
                method: "PATCH",
                headers: {
                  Authorization: `Bearer ${token}`,
                  "Content-Type": "application/json",
                },
                body: JSON.stringify({ name: "Архив" }),
              },
            );
            const data = await res.json();
        - lang: Python
          source: |
            import requests
            r = requests.patch(
                f"https://app.coverse.team/v1/documents/{document_id}/sheets/{sheet_id}",
                headers={"Authorization": f"Bearer {token}"},
                json={"name": "Архив"},
            )
            print(r.json())
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name:
                  type: string
                  maxLength: 255
      responses:
        "200":
          description: Переименованный лист.
          content:
            application/json:
              schema:
                type: object
                properties:
                  sheet:
                    $ref: "#/components/schemas/Sheet"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
    delete:
      tags: [Листы]
      summary: Удалить лист
      description: Удаляет лист. Требуется право `sheets.write`.
      operationId: deleteSheet
      x-codeSamples:
        - lang: cURL
          source: |
            curl -X DELETE "https://app.coverse.team/v1/documents/$DOCUMENT_ID/sheets/$SHEET_ID" \
              -H "Authorization: Bearer $TOKEN"
        - lang: JavaScript
          source: |
            const res = await fetch(
              `https://app.coverse.team/v1/documents/${documentId}/sheets/${sheetId}`,
              { method: "DELETE", headers: { Authorization: `Bearer ${token}` } },
            );
            const data = await res.json();
        - lang: Python
          source: |
            import requests
            r = requests.delete(
                f"https://app.coverse.team/v1/documents/{document_id}/sheets/{sheet_id}",
                headers={"Authorization": f"Bearer {token}"},
            )
            print(r.json())
      responses:
        "200":
          description: Удалённый лист.
          content:
            application/json:
              schema:
                type: object
                properties:
                  deleted:
                    $ref: "#/components/schemas/Sheet"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"

  /documents/{document}/webhooks:
    parameters:
      - $ref: "#/components/parameters/DocumentId"
    get:
      tags: [Вебхуки]
      summary: Список вебхуков
      description: Возвращает подписки-вебхуки документа. Требуется право `sheets.webhooks`.
      operationId: listWebhooks
      x-codeSamples:
        - lang: cURL
          source: |
            curl "https://app.coverse.team/v1/documents/$DOCUMENT_ID/webhooks" \
              -H "Authorization: Bearer $TOKEN"
        - lang: JavaScript
          source: |
            const res = await fetch(
              `https://app.coverse.team/v1/documents/${documentId}/webhooks`,
              { headers: { Authorization: `Bearer ${token}` } },
            );
            const data = await res.json();
        - lang: Python
          source: |
            import requests
            r = requests.get(
                f"https://app.coverse.team/v1/documents/{document_id}/webhooks",
                headers={"Authorization": f"Bearer {token}"},
            )
            print(r.json())
      responses:
        "200":
          description: Подписки.
          content:
            application/json:
              schema:
                type: object
                properties:
                  webhooks:
                    type: array
                    items:
                      $ref: "#/components/schemas/Webhook"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
    post:
      tags: [Вебхуки]
      summary: Создать вебхук
      description: >
        Создаёт подписку-вебхук. Требуется право `sheets.webhooks`. Для канала
        `http` в ответе один раз возвращается секрет подписи — больше он не
        показывается. Для канала `telegram` вместо `url` укажите `botToken` и
        `chatId`.
      operationId: createWebhook
      x-codeSamples:
        - lang: cURL
          source: |
            curl -X POST "https://app.coverse.team/v1/documents/$DOCUMENT_ID/webhooks" \
              -H "Authorization: Bearer $TOKEN" \
              -H "Content-Type: application/json" \
              -d '{"channel":"http","url":"https://example.com/hooks/coverse"}'
        - lang: JavaScript
          source: |
            const res = await fetch(
              `https://app.coverse.team/v1/documents/${documentId}/webhooks`,
              {
                method: "POST",
                headers: {
                  Authorization: `Bearer ${token}`,
                  "Content-Type": "application/json",
                },
                body: JSON.stringify({ channel: "http", url: "https://example.com/hooks/coverse" }),
              },
            );
            const data = await res.json();
        - lang: Python
          source: |
            import requests
            r = requests.post(
                f"https://app.coverse.team/v1/documents/{document_id}/webhooks",
                headers={"Authorization": f"Bearer {token}"},
                json={"channel": "http", "url": "https://example.com/hooks/coverse"},
            )
            print(r.json())
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                channel:
                  type: string
                  enum: [http, telegram]
                  default: http
                url:
                  type: string
                  format: uri
                  maxLength: 2048
                  description: Обязателен для канала `http`.
                botToken:
                  type: string
                  maxLength: 255
                  description: Обязателен для канала `telegram`.
                chatId:
                  type: string
                  maxLength: 64
                  description: Обязателен для канала `telegram`.
                events:
                  type: array
                  minItems: 1
                  items:
                    type: string
                    enum: [document.updated]
            examples:
              http:
                summary: Канал http
                value:
                  channel: http
                  url: https://example.com/hooks/coverse
              telegram:
                summary: Канал Telegram
                value:
                  channel: telegram
                  botToken: "123456:ABC-DEF…"
                  chatId: "-1001234567890"
      responses:
        "201":
          description: Созданная подписка. Поле `secret` присутствует только для канала http.
          content:
            application/json:
              schema:
                type: object
                properties:
                  webhook:
                    $ref: "#/components/schemas/Webhook"
                  secret:
                    type: string
                    description: Секрет подписи, показывается один раз.
              example:
                webhook:
                  id: 15
                  channel: http
                  events: [document.updated]
                  active: true
                  createdAt: "2026-07-09T09:20:00+00:00"
                  url: https://example.com/hooks/coverse
                  token: Интеграция с CRM
                secret: whsec_8f3a9b2c1d4e5f6a7b8c9d0e1f2a3b4c
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "422":
          description: Ошибка валидации либо достигнут лимит вебхуков тарифа.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /documents/{document}/webhooks/{webhook}:
    parameters:
      - $ref: "#/components/parameters/DocumentId"
      - name: webhook
        in: path
        required: true
        description: Числовой идентификатор подписки-вебхука.
        schema:
          type: integer
    delete:
      tags: [Вебхуки]
      summary: Удалить вебхук
      description: Удаляет подписку-вебхук. Требуется право `sheets.webhooks`.
      operationId: deleteWebhook
      x-codeSamples:
        - lang: cURL
          source: |
            curl -X DELETE "https://app.coverse.team/v1/documents/$DOCUMENT_ID/webhooks/$WEBHOOK_ID" \
              -H "Authorization: Bearer $TOKEN"
        - lang: JavaScript
          source: |
            const res = await fetch(
              `https://app.coverse.team/v1/documents/${documentId}/webhooks/${webhookId}`,
              { method: "DELETE", headers: { Authorization: `Bearer ${token}` } },
            );
            const data = await res.json();
        - lang: Python
          source: |
            import requests
            r = requests.delete(
                f"https://app.coverse.team/v1/documents/{document_id}/webhooks/{webhook_id}",
                headers={"Authorization": f"Bearer {token}"},
            )
            print(r.json())
      responses:
        "200":
          description: Удалено.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"

  /documents/{document}/audit:
    parameters:
      - $ref: "#/components/parameters/DocumentId"
    get:
      tags: [Аудит]
      summary: Прочитать журнал изменений
      description: >
        Возвращает журнал изменений документа, сделанных через API и интеграции.
        Требуется право `sheets.read` и тариф с аудитом (Профессиональный и
        выше); иначе возвращается `403`.
      operationId: readAudit
      x-codeSamples:
        - lang: cURL
          source: |
            curl "https://app.coverse.team/v1/documents/$DOCUMENT_ID/audit?limit=50" \
              -H "Authorization: Bearer $TOKEN"
        - lang: JavaScript
          source: |
            const res = await fetch(
              `https://app.coverse.team/v1/documents/${documentId}/audit?limit=50`,
              { headers: { Authorization: `Bearer ${token}` } },
            );
            const data = await res.json();
        - lang: Python
          source: |
            import requests
            r = requests.get(
                f"https://app.coverse.team/v1/documents/{document_id}/audit",
                params={"limit": 50},
                headers={"Authorization": f"Bearer {token}"},
            )
            print(r.json())
      parameters:
        - name: offset
          in: query
          required: false
          schema:
            type: integer
            minimum: 0
            default: 0
        - name: limit
          in: query
          required: false
          description: Ограничивается значением 200.
          schema:
            type: integer
            minimum: 1
            default: 50
      responses:
        "200":
          description: Страница журнала изменений.
          content:
            application/json:
              schema:
                type: object
                properties:
                  audit:
                    type: object
                    properties:
                      entries:
                        type: array
                        items:
                          $ref: "#/components/schemas/AuditEntry"
                      total:
                        type: integer
                      offset:
                        type: integer
                      limit:
                        type: integer
                      tokens:
                        type: array
                        items:
                          type: string
              example:
                audit:
                  entries:
                    - id: 204
                      action: update
                      origin: api
                      userId: 7
                      userName: Павел Власов
                      token: Интеграция с CRM
                      properties:
                        range: Лист 1!A2:B2
                        cells: 2
                      createdAt: "2026-07-09T09:15:14+00:00"
                  total: 1
                  offset: 0
                  limit: 50
                  tokens: [Интеграция с CRM]
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"

webhooks:
  document.updated:
    post:
      tags: [Вебхуки]
      summary: Уведомление об изменении документа
      operationId: onDocumentUpdated
      description: >
        Это исходящий запрос, который Coverse отправляет на `url` вашей http-подписки
        при изменении документа. Настраивается через `POST /documents/{document}/webhooks`.


        Доставка сгруппирована (дебаунс): близкие по времени правки объединяются в
        одно уведомление на документ. Тело содержит изменённые ячейки на момент
        доставки. Ваш обработчик должен ответить кодом `2xx`, иначе доставка
        считается неуспешной и повторяется с задержкой.


        Проверка подписи (обязательно для безопасности). В заголовке
        `X-Coverse-Signature` приходит `sha256=<hex>`, где `<hex>` — это
        HMAC-SHA256 от **сырого тела запроса** с ключом, равным секрету подписи
        (`secret`, выданному один раз при создании подписки). Сравнивайте
        подпись по сырым байтам тела, до любого разбора JSON. Пример на PHP:
        `hash_hmac('sha256', $rawBody, $secret)`.
      parameters:
        - name: X-Coverse-Event
          in: header
          description: Имя события, например `document.updated`.
          schema:
            type: string
        - name: X-Coverse-Signature
          in: header
          description: "Подпись тела: `sha256=<hex HMAC-SHA256(secret, rawBody)>`."
          schema:
            type: string
        - name: X-Coverse-Timestamp
          in: header
          description: Момент доставки в формате ISO 8601 (защита от повторной отправки).
          schema:
            type: string
            format: date-time
      requestBody:
        description: Тело уведомления.
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/WebhookDelivery"
            example:
              schemaVersion: "1.0"
              event: document.updated
              source: api
              sourceToken: Интеграция с CRM
              documentId: 3f2b9c10-8a4e-4f21-9b7d-2c5e6a1f0d34
              documentTitle: Заявки
              documentUrl: https://app.coverse.team/files/document/3f2b9c10-8a4e-4f21-9b7d-2c5e6a1f0d34
              workspace:
                id: 42
                name: Отдел продаж
              revision:
                from: 118
                to: 121
              changedAt: "2026-07-09T09:15:04+00:00"
              authors:
                - id: 7
                  name: Павел Власов
              cellCount: 2
              changes: Лист 1!E21 = 3456; Лист 1!F21 = Новый
              ranges:
                - Лист 1!E21:F21
              cells:
                - sheet: "0"
                  sheetName: Лист 1
                  ref: E21
                  row: 20
                  col: 4
                  v: 3456
                  m: "3456"
                  f: null
                  t: number
                - sheet: "0"
                  sheetName: Лист 1
                  ref: F21
                  row: 20
                  col: 5
                  v: Новый
                  m: Новый
                  f: null
                  t: string
              truncated: false
              deliveryId: 9a1c2d3e-4f5a-6b7c-8d9e-0f1a2b3c4d5e
              attempt: 1
              deliveredAt: "2026-07-09T09:15:14+00:00"
      responses:
        "200":
          description: Верните любой код `2xx`, чтобы подтвердить получение.

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: >
        Токен доступа, созданный в настройках аккаунта Coverse («Автоматизация и
        API»). Передавайте его как `Authorization: Bearer <токен>`. У каждого
        токена есть права (scopes): `sheets.read`, `sheets.write`,
        `sheets.webhooks`, `sheets.crossbook.read`, `sheets.agent.write`.
        Эндпоинт требует право, указанное в его описании; токен без нужного права
        получает `403`.

  parameters:
    DocumentId:
      name: document
      in: path
      required: true
      description: Идентификатор документа (UUID).
      schema:
        type: string
        format: uuid
    Range:
      name: range
      in: path
      required: true
      description: >
        Диапазон в A1-нотации, URL-кодированный. Примеры: `A1`, `A1:C10`,
        `Sheet1!A1:C10`, `'Мой лист'!B2`, открытые диапазоны `A:A`, `1:1`, `A1:A`.
      schema:
        type: string
    SheetId:
      name: sheet
      in: path
      required: true
      description: Идентификатор листа, как его возвращают эндпоинты листов (`sheetId`).
      schema:
        type: string
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      description: >
        Необязательный уникальный ключ, чтобы запись можно было безопасно
        повторить. Повторный запрос с тем же ключом вернёт исходный результат, а
        не запишет дважды.
      schema:
        type: string

  responses:
    Unauthorized:
      description: Токен доступа отсутствует или недействителен.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    Forbidden:
      description: У токена нет нужного права либо тариф этого не позволяет.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    NotFound:
      description: Документ или лист не существует или недоступен.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    UnprocessableEntity:
      description: Запрос некорректен (неверный диапазон, не тот тип документа, неправильное тело).
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    RateLimited:
      description: >
        Слишком много запросов. Базовый лимит — 60 запросов в минуту на токен;
        тарифы могут его повышать.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"

  schemas:
    Error:
      type: object
      properties:
        message:
          type: string
      required: [message]

    Document:
      type: object
      properties:
        id:
          type: string
          format: uuid
        type:
          type: string
          examples: [table]
        title:
          type: [string, "null"]
        revisionId:
          type: string
        createdAt:
          type: [string, "null"]
          format: date-time
        updatedAt:
          type: [string, "null"]
          format: date-time
        content:
          type: object
          description: Присутствует для таблиц. Ссылки на вычисленное содержимое и стили.
          properties:
            contentUrl:
              type: [string, "null"]
            stylesUrl:
              type: [string, "null"]

    Cell:
      description: >
        Одна ячейка либо `null` для пустой ячейки. `v` — исходное значение, `m` —
        отображаемая строка, `f` — формула (только у формульных ячеек).
      oneOf:
        - type: "null"
        - type: object
          properties:
            v:
              description: Исходное значение (число, строка, логическое или null).
              type: [string, number, boolean, "null"]
            m:
              type: [string, "null"]
              description: Отформатированная отображаемая строка.
            f:
              type: string
              description: Формула, присутствует только у формульных ячеек.

    ValueMatrix:
      type: array
      minItems: 1
      description: >
        Матрица значений для записи, по строкам. Каждый вложенный массив — строка.
        При `USER_ENTERED` ведущий `=` в строке считается формулой.
      items:
        type: array
        items:
          type: [string, number, boolean, "null"]

    ValueInputOption:
      type: string
      enum: [RAW, USER_ENTERED]
      default: USER_ENTERED
      description: >
        `USER_ENTERED` разбирает ввод как при наборе пользователем (формулы,
        распознавание чисел и дат). `RAW` записывает значения как есть.

    ValueRange:
      type: object
      properties:
        sheet:
          type: [string, "null"]
        range:
          type: string
          description: Конкретный разрешённый диапазон (открытые диапазоны разворачиваются).
        majorDimension:
          type: string
          enum: [ROWS]
        cells:
          type: array
          description: Сетка по строкам. Каждая строка — массив ячеек; пустые ячейки — `null`.
          items:
            type: array
            items:
              $ref: "#/components/schemas/Cell"
        pagination:
          $ref: "#/components/schemas/Pagination"

    Pagination:
      type: object
      properties:
        offset:
          type: integer
        limit:
          type: integer
        total:
          type: integer
        returned:
          type: integer
        hasMore:
          type: boolean
        nextOffset:
          type: [integer, "null"]

    Sheet:
      type: object
      properties:
        sheetId:
          type: string
          description: Стабильный идентификатор листа, используется в путях листов.
        uid:
          type: [string, "null"]
        name:
          type: [string, "null"]
        hidden:
          type: boolean
        order:
          type: integer
        color:
          type: [string, "null"]

    Webhook:
      type: object
      properties:
        id:
          type: integer
        channel:
          type: string
          enum: [http, telegram]
        events:
          type: array
          items:
            type: string
        active:
          type: boolean
        createdAt:
          type: [string, "null"]
          format: date-time
        url:
          type: [string, "null"]
          description: Присутствует для канала http.
        token:
          type: [string, "null"]
          description: Имя токена, создавшего подписку (канал http).
        chatId:
          type: [string, "null"]
          description: Присутствует для канала telegram.

    AuditEntry:
      type: object
      properties:
        id:
          type: integer
        action:
          type: string
          examples: [update, batchUpdate, append, clear, sheet.create, sheet.rename, sheet.delete]
        origin:
          type: [string, "null"]
        userId:
          type: [integer, "null"]
        userName:
          type: [string, "null"]
        token:
          type: [string, "null"]
        properties:
          type: [object, "null"]
          description: Детали действия, например затронутый диапазон и число ячеек.
        createdAt:
          type: [string, "null"]
          format: date-time

    WebhookDelivery:
      type: object
      description: Тело уведомления, которое Coverse отправляет на url http-подписки.
      properties:
        schemaVersion:
          type: string
          description: Версия схемы payload.
        event:
          type: string
          examples: [document.updated]
        source:
          type: string
          enum: [api, browser]
          description: Откуда пришло изменение — через API или из браузера.
        sourceToken:
          type: [string, "null"]
          description: Имя токена-интеграции для изменений через API; для браузера — null.
        documentId:
          type: [string, "null"]
          format: uuid
        documentTitle:
          type: [string, "null"]
        documentUrl:
          type: [string, "null"]
          description: Прямая ссылка на документ.
        workspace:
          type: [object, "null"]
          properties:
            id:
              type: integer
            name:
              type: [string, "null"]
        revision:
          type: object
          description: Диапазон ревизий, охваченных этим уведомлением.
          properties:
            from:
              type: [integer, "null"]
            to:
              type: [integer, "null"]
        changedAt:
          type: [string, "null"]
          format: date-time
        authors:
          type: array
          items:
            type: object
            properties:
              id:
                type: [integer, "null"]
              name:
                type: [string, "null"]
        cellCount:
          type: integer
          description: Общее число изменённых ячеек (даже если часть не вошла в `cells`).
        changes:
          type: string
          description: >
            Готовая строка-сводка всех включённых ячеек, например
            `Лист 1!E21 = 3456; …` — удобно для no-code инструментов.
        ranges:
          type: array
          description: Изменённые ячейки, схлопнутые в горизонтальные диапазоны A1.
          items:
            type: string
        cells:
          type: array
          items:
            type: object
            properties:
              sheet:
                type: string
              sheetName:
                type: [string, "null"]
              ref:
                type: string
                description: Адрес ячейки в A1, например `E21`.
              row:
                type: integer
                description: Номер строки (от 0).
              col:
                type: integer
                description: Номер столбца (от 0).
              v:
                type: [string, number, boolean, "null"]
              m:
                type: [string, "null"]
              f:
                type: [string, "null"]
              t:
                type: [string, "null"]
                enum: [number, boolean, string, null]
                description: Тип значения `v`.
        truncated:
          type: boolean
          description: >
            `true`, если ячеек больше лимита payload — тогда `cells` обрезан, а
            `cellCount` показывает полное число.
        deliveryId:
          type: string
          format: uuid
          description: Идентификатор доставки, стабилен при повторах.
        attempt:
          type: integer
          description: Номер попытки доставки (растёт при повторах).
        deliveredAt:
          type: [string, "null"]
          format: date-time
