APIНачало работы

Ошибки

Формат

Ошибка — это всегда JSON. Минимальный вид:

json
{
  "statusCode": 404,
  "message": "Документ не найден",
  "error": "Not Found"
}

message написан по-русски и предназначен человеку: его можно показать в интерфейсе как есть. Внутренних подробностей — путей, SQL, стека — в ответе нет никогда; они остаются в журнале сервера.

Ошибка проверки тела запроса дополнительно перечисляет поля:

json
{
  "message": "Назовите токен — потом не разберётесь",
  "errors": [
    { "field": "name", "message": "Назовите токен — потом не разберётесь" }
  ]
}
  • errors[].field — путь к полю через точку, например data.name или rows.3.1.
  • errors[].message — что именно не так.
  • message — сводка: до трёх разных причин через точку с запятой, дальше «— и ещё N». Длинное перечисление никто не дочитывает.

Ответы с дополнительными полями — проверка полей, конфликт версии листа, превышение частоты, сбой превью — приходят без statusCode и error: в теле только message и свои поля. Поэтому код ошибки берите из статуса HTTP, а не из тела.

Коды

КодЧто значитЧто делать
200Успех—
201Объект создан. Ответ на POST, если не указано иное—
400Тело или параметры не прошли проверку — либо состояние не позволяет действие: выпуск по материалу уже идёт, остатка документов не хватает, срок плана вышелИсправить поля из errors; для остального прочитать message и текущее состояние (GET /api/org/usage, задание выпуска)
401Нет токена, либо он неизвестен или отозванПроверить заголовок Authorization
403Не хватает роли, действие только для человека, возможности нет в плане или срок плана вышелСм. Аутентификация
404Объекта нет — или он принадлежит другой организацииПроверить идентификатор
409Конфликт: лист изменился с тех пор, как его прочитали (правка листа с baseVersion); проект с таким названием уже есть (POST /api/projects, PATCH /api/projects/{id} и их устаревшие синонимы /api/folders); Idempotency-Key уже использован для другого документа (POST /api/documents/{id}/issue)Для листа — перечитать и повторить с новой версией (см. ниже); для проекта — другое название; для выдачи — новый ключ на каждого получателя
413Тело больше допустимогоРазбить на части; предел — 1 МБ, у импорта 16 МБ, у /mcp 2 МБ
422Превью листа не отрисовалось (POST …/sheets/{sheetId}/preview)Проверить лист (POST …/check) и fileId картинок, повторить
429Слишком часто: с одного адреса — человек и публичные маршруты; от организации — запросы по токену, MCP, превью и выдача по APIПодождать столько секунд, сколько названо в message, см. Ограничения частоты
500Ошибка на нашей сторонеПовторить позже; текст ответа обобщённый
503Внешняя служба недоступна: очередь выпуска или печати, почтовый сервис при подключении домена, оплата картой не подключена; превью не успело отрисоваться за 30 секундПовторить позже; при выпуске сперва найти уже заведённое задание, см. AGENTS.md

Конфликт версии листа

У листа есть номер версии version — он растёт с каждым сохранением. Правка листа (PATCH …/sheets/{sheetId}, PATCH …/sheets/{sheetId}/elements, инструменты MCP edit_layout и set_layout) принимает необязательный baseVersion — версию, которую программа видела. Если лист за это время изменился, ответ 409, и в теле есть текущая версия:

json
{
  "message": "Лист изменился (версия 8, а правка сделана к версии 7). Перечитайте лист и повторите правку.",
  "version": 8
}

Перечитайте лист (GET /api/documents/{id} или get_document), примените правку к свежему листу и передайте baseVersion: 8. Без baseVersion последнее сохранение выигрывает. Редактор в кабинете пишет с версией: чужую правку он не затирает, а спрашивает человека, чья версия остаётся.

Если лист правят одновременно слишком часто и правка четыре раза подряд не застала его неизменным, ответ тоже 409, без version: «Лист правят одновременно слишком часто — повторите запрос». Так бывает и у правок без baseVersion, например у готовой раскладки.

Проверка подлинности — своя форма ответа

Публичная проверка подлинности GET /api/v1/verify/{id} отвечает на ненайденный документ телом другой формы — без statusCode и error, зато с полем reason:

json
{ "message": "Такого документа нет", "reason": "checksum" }

reason принимает значения malformed (код не той длины или с посторонними знаками), checksum (опечатка — контрольная часть кода не сошлась) и unknown (код правильный, но такого документа нет). Различие нужно, чтобы сказать проверяющему «проверьте, не ошиблись ли при наборе» вместо «документа не существует».

Ошибки входа через OAuth

Адреса OAuth (POST /api/oauth/register, POST /api/oauth/token) разбирают клиенты OAuth, а не люди, поэтому отвечают в формате RFC 6749 — полями error и error_description, без statusCode:

json
{
  "error": "invalid_grant",
  "error_description": "Код неверный, просрочен или уже использован"
}
errorКогда
invalid_client_metadataПри регистрации нет redirect_uris или клиент не публичный (token_endpoint_auth_method не none)
invalid_redirect_uriАдрес возврата не годится: http не на localhost, фрагмент #, опасная схема
unsupported_grant_typegrant_type не authorization_code
invalid_requestНет code, code_verifier или client_id
invalid_grantКод неверный, просрочен (живёт 10 минут) или уже использован; выдан другому клиенту; не совпал redirect_uri или PKCE; разрешивший больше не управляющий; у организации уже десять подключений

Страница согласия https://vruchay.ru/oauth/authorize показывает ошибки человеку по-русски: «Неизвестное приложение: client_id не наш или повреждён», «Адрес возврата не совпадает с зарегистрированным у приложения», «Нужны права управляющего». Отказ человека возвращает приложению error=access_denied в адресе возврата.

Сервер MCP без токена или с неизвестным токеном отвечает 401 с телом {"error": "unauthorized", "message": "…"} и заголовком WWW-Authenticate: Bearer resource_metadata="https://vruchay.ru/.well-known/oauth-protected-resource/mcp" (с неизвестным токеном — ещё error="invalid_token"): по нему клиент начинает вход.

Ошибки в MCP

Ошибка инструмента — это не ошибка протокола. Запрос к /mcp отвечает 200, а в результате вызова стоит isError: true и текст по-русски — тот же, что вернул бы REST:

json
{
  "jsonrpc": "2.0",
  "id": 7,
  "result": {
    "isError": true,
    "content": [
      {
        "type": "text",
        "text": "Ошибка 400: Введите название\nПоля: [{\"field\":\"title\",\"message\":\"Введите название\"}]"
      }
    ]
  }
}

Так агент видит причину и может поправить запрос, а не обрывает работу. Аргументы, не подошедшие схеме инструмента, — тоже isError с текстом «Аргументы не подошли — …».

Ошибкой протокола JSON-RPC (error вместо result) отвечают только на неверное сообщение: -32600 — не JSON-RPC 2.0 или пачка сообщений (и превышение 300 запросов в минуту, вместе с HTTP 429 и Retry-After), -32601 — неизвестный метод, -32602 — неизвестный инструмент или подсказка либо arguments не объект. Клиенты протокола 2026-07-28 получают ещё -32020 (заголовки MCP-Protocol-Version, Mcp-Method, Mcp-Name не совпали с телом, HTTP 400) и -32022 (незнакомая версия, HTTP 400, в data.supported — версии сервера), а неизвестный метод у них — HTTP 404. Подробно — в `POST /mcp`.

Почему 404, а не 403

Обращение к объекту чужой организации отвечает 404. Ответ 403 подтвердил бы, что объект с таким идентификатором существует, — а это уже сведения о чужих делах. Поэтому «не ваш» и «не существует» для API неразличимы.

Разбор ошибок в программе

Ориентируйтесь на статус HTTP и на errors[].field. Тексты сообщений живут в схемах проверки и меняются вместе с ними: сопоставлять их построчно — способ однажды сломаться на исправленной опечатке.

Ошибки с кодами 4xx повторять бессмысленно, пока не изменился запрос. Повторять стоит 429 (после паузы) и 500 (с нарастающей задержкой).

Вручай