Ошибки
Формат
Ошибка — это всегда JSON. Минимальный вид:
{
"statusCode": 404,
"message": "Документ не найден",
"error": "Not Found"
}message написан по-русски и предназначен человеку: его можно показать в интерфейсе как есть. Внутренних подробностей — путей, SQL, стека — в ответе нет никогда; они остаются в журнале сервера.
Ошибка проверки тела запроса дополнительно перечисляет поля:
{
"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, и в теле есть текущая версия:
{
"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:
{ "message": "Такого документа нет", "reason": "checksum" }reason принимает значения malformed (код не той длины или с посторонними знаками), checksum (опечатка — контрольная часть кода не сошлась) и unknown (код правильный, но такого документа нет). Различие нужно, чтобы сказать проверяющему «проверьте, не ошиблись ли при наборе» вместо «документа не существует».
Ошибки входа через OAuth
Адреса OAuth (POST /api/oauth/register, POST /api/oauth/token) разбирают клиенты OAuth, а не люди, поэтому отвечают в формате RFC 6749 — полями error и error_description, без statusCode:
{
"error": "invalid_grant",
"error_description": "Код неверный, просрочен или уже использован"
}error | Когда |
|---|---|
invalid_client_metadata | При регистрации нет redirect_uris или клиент не публичный (token_endpoint_auth_method не none) |
invalid_redirect_uri | Адрес возврата не годится: http не на localhost, фрагмент #, опасная схема |
unsupported_grant_type | grant_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:
{
"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 (с нарастающей задержкой).