SIRINVIDEO CCTV Документация администратора и пользователя На сайт 2026.08

Введение и аутентификация

Базовый URL, получение токена, роли и права, формат ошибок, постраничная выдача и потоки событий.

REST API контрол-плейна — то же самое, чем пользуется веб-админка: в системе нет действия, доступного в интерфейсе и недоступного в API. Через него делают массовый завод камер, ночную выгрузку архива, интеграцию со СКУД и системами учёта, внешний мониторинг флота.

Эта страница описывает то, что одинаково для всех маршрутов: адрес, токен, роли, коды ошибок, постраничную выдачу и потоки SSE. Разбор конкретных маршрутов — в соседних разделах, начиная с Камеры.

Базовый URL#

https://cctv.example.com/api/v1
  • Схема — только HTTPS. Единственное исключение — режим http, когда перед Plane стоит реверс-прокси, который и завершает TLS; наружу всё равно смотрит HTTPS.
  • Хост и порт — те же, что у админки. Отдельного порта под API нет.
  • Тело запроса и ответа — всегда JSON в UTF-8. Для запросов с телом обязателен заголовок Content-Type: application/json.

Во всех примерах ниже используются две переменные оболочки:

BASE=https://cctv.example.com     # адрес вашего Plane, без завершающего слэша
TOKEN=                           # токен сессии, получение — ниже
Проверка, что API вообще отвечает
curl -sS -o /dev/null -w "%{http_code}\n" "$BASE/api/v1/version"
# 401 — сервис работает и требует авторизации. Это ожидаемый ответ без токена.

Ответ 000 означает, что до сервиса не дошли по сети, 502/504 — проблема на реверс-прокси.

Аутентификация#

POST /api/v1/auth/login#

Без токена — единственный маршрут /api/v1, не требующий авторизации.

Назначение — обменять логин и пароль на подписанный токен сессии. Plane при этом создаёт запись сессии в базе, обновляет last_login_at пользователя и пишет запись аудита auth.login.

Тело запроса

Поле Тип Обязательное
login строка да
password строка да

Пример curl — рабочий шаблон, которым начинается любой сценарий в этой документации:

BASE=https://cctv.example.com

TOKEN=$(curl -sS -X POST "$BASE/api/v1/auth/login" \
  -H 'Content-Type: application/json' \
  -d '{"login":"integration","password":"<ПАРОЛЬ>"}' \
  | python3 -c 'import sys,json;print(json.load(sys.stdin)["token"])')

# проверка: непустая строка вида eyJhbGciOi...
echo "${TOKEN:0:16}…"

Пример ответа

{
  "token": "eyJhbGciOi…",
  "expires_at": "2026-08-19T12:00:00Z",
  "jti": "<UUID>",
  "sub": "<UUID>",
  "org": "<UUID>",
  "role": "org_admin"
}

sub — идентификатор пользователя, org — его организация, jti — идентификатор сессии (он же попадает в аудит и в списки отзыва). Роль и организация в теле ответа даны для удобства интерфейса; полагаться на них как на источник истины не нужно — см. ниже.

Ошибки

Код Тело Причина
401 {"error":"bad_credentials"} неизвестный логин, неверный пароль или отключённая учётная запись — эти случаи намеренно неразличимы
429 {"error":"throttled"} этот IP временно заблокирован после серии неудач
503 {"error":"db_unavailable"} база или обработчик паролей недоступны

Примечания

  • Защита от перебора: 8 неудачных попыток с одного IP за 60 секунд блокируют этот IP на 60 секунд; успешный вход счётчик обнуляет. IP берётся из первого узла X-Forwarded-For, если заголовок есть, иначе из TCP-соединения. Скрипт, который в цикле пробует пароли, заблокирует сам себя — вставляйте паузу и не запускайте вход параллельно из нескольких процессов.
  • В журнал аудита пишутся auth.login, auth.login_failed (с указанием введённого логина), auth.lockout и auth.session_evicted.

Заголовок авторизации#

Все остальные маршруты /api/v1 требуют заголовок:

curl -sS "$BASE/api/v1/cameras?limit=50" \
  -H "Authorization: Bearer $TOKEN"

Ни cookie-авторизации, ни API-ключей для этих маршрутов нет. Модульные токены существуют, но открывают только приём событий (POST /api/v1/events) и описаны в разделе Настройки Plane и токены.

Роль и организация читаются из базы, а не из токена

При каждом запросе Plane заново проверяет подпись токена, наличие живой (не отозванной и не просроченной) сессии, а затем перечитывает пользователя из базы. Поля role и org внутри токена — только подсказка для интерфейса. Практическое следствие: понижение роли, отключение учётной записи или отзыв сессии действуют со следующего же запроса, а не после истечения токена.

Срок жизни токена#

expires_at в ответе на вход — это текущее время + session_ttl_secs, значение задаётся на весь Plane в plane.toml (см. plane.toml — все параметры). После этого момента любой запрос вернёт 401 unauthorized.

Долгоживущей интеграции не нужно логиниться заново: обновляйте текущую сессию.

POST /api/v1/session/refresh#

Любой вошедший

Назначение — переподписать существующую сессию: тот же идентификатор сессии (jti), новый срок действия, заново прочитанные из базы роль, организация и гранты. Пользуйтесь этим, чтобы держать долгую интеграцию живой и чтобы подхватить изменение прав без повторного входа.

Параметры пути, параметры запроса, тело — отсутствуют. Токен берётся из заголовка.

Пример curl

TOKEN=$(curl -sS -X POST "$BASE/api/v1/session/refresh" \
  -H "Authorization: Bearer $TOKEN" \
  | python3 -c 'import sys,json;print(json.load(sys.stdin)["token"])')

Пример ответа

{ "token": "eyJhbGciOi…", "expires_at": "2026-08-19T12:00:00Z" }

Ошибки

Код Тело Причина
401 {"error":"evicted"} сессия отозвана: её вытеснил чужой вход, сработала админская операция безопасности или была ротация ключа подписи
401 {"error":"unauthorized"} токена нет, он повреждён, уже просрочен, сессия неизвестна, пользователь удалён или отключён
503 {"error":"db_unavailable"} база недоступна

Примечания

  • Идентификатор сессии jti сохраняется — записи аудита и списки отзыва на нодах остаются корректными.
  • Обновление делает сессию самой свежей, поэтому она переживает вытеснение по лимиту сессий.
  • Различие evicted и unauthorized сделано специально: клиент может отличить «меня выкинули» от «токен просто негоден» и по-разному сообщить об этом оператору.
  • Обновляйте токен заранее — например, за 10 % до expires_at, а не по факту получения 401.

Роли и права#

Ролей три. Роль хранится у пользователя и определяет доступ к административным маршрутам; у оператора доступ к камерам дополнительно ограничен грантами.

Роль В API Что может
Суперадминистратор superadmin всё во всей установке: организации, ноды, лицензии, перенос объектов между организациями, выдача роли суперадмина
Администратор организации org_admin всё внутри своей организации: камеры, пользователи, теги, шаблоны, мозаики, ссылки доступа
Пользователь (оператор) user только то, что выдано грантами: просмотр, архив, события, PTZ, экспорт

Суперадминистратор. Живёт только в организации по умолчанию. Последний включённый суперадминистратор защищён: его нельзя удалить, отключить или понизить в роли — попытка вернёт 409 last_superadmin. Только суперадминистратор может создавать и редактировать организации, переносить пользователя, тег, камеру или шаблон в другую организацию и выдавать роль superadmin.

Администратор организации. Видит и меняет всё в своей организации, но не может изменить саму организацию — квоты и лимиты редактирует только суперадминистратор. Не может ни создать суперадминистратора, ни как-либо его затронуть (403 superadmin_is_superadmin_only).

Оператор. Административные списки (GET /api/v1/cameras, /users, /tags) для него закрыты — 403 forbidden. Ему доступны: собственная учётная запись и смена своего пароля, компактный список камер GET /api/v1/cameras/lite, мобильная витрина /api/v1/mobile/*, дескрипторы просмотра и архива, PTZ и экспорт — в объёме выданных бит привилегий.

Биты привилегий#

Грант — это маска привилегий, выданная на организацию целиком или на конкретный тег. Итоговое право оператора на камеру — объединение (побитовое ИЛИ) масок всех гранта-меток, которые несёт камера: её организация и её теги.

Бит Значение Что открывает
LIVE 1 живое видео
DVR 2 воспроизведение архива
EVENTS 4 просмотр событий
PTZ 8 управление поворотом и зумом
EXPORT 16 выгрузка фрагмента архива
PREVIEW 32 статичный кадр-превью

Биты независимы: просмотр архива не подразумевает выгрузку, а превью не подразумевает живое видео. Полная маска — 63 (0x3f). Работа с грантами описана в разделе Организации и пользователи и Пользователи и права доступа.

Сужение области видимости: ?org=#

Многие административные списки принимают ?org=<UUID>.

  • Суперадминистратор без ?org= видит всю установку, с ?org= — одну организацию.
  • Любая другая роль, указавшая чужую организацию, получает 403 cross_org. Молчаливого отката «к своей организации» не происходит никогда — это осознанное решение, чтобы ошибка в скрипте не выглядела как успех.
Одно исключение из правила «без ?org= — вся установка»

Список шаблонов камер всегда сужен ровно до одной организации: суперадминистратор без ?org= увидит шаблоны своей организации, а не всей установки. Чтобы посмотреть чужие, указывайте ?org= явно.

Формат ошибок#

Тело ошибки всегда одинаковой формы — объект с одним полем error, в котором лежит машинный код, пригодный для сравнения в коде:

{ "error": "camera_limit" }

Некоторые ошибки добавляют поля с подробностями (например, detail и retry_after_seconds у отказов со стороны камеры) — они описаны там, где встречаются. Человекочитаемых текстов на русском API не возвращает: сообщение для оператора формирует клиент.

Код Что означает в этом API Типичные значения error
400 запрос синтаксически разобран, но не прошёл проверку значений name_empty, bad_camera_status, bad_ttl, bad_range
401 не аутентифицирован: токена нет, он негоден, просрочен, сессия отозвана, пользователь удалён или отключён unauthorized, evicted, bad_credentials
403 аутентифицирован, но не та роль или не та организация forbidden, not_admin, cross_org, no_dvr, no_export
404 объект не существует или не виден вызывающему not_found
409 конфликт состояния: лимит, дубликат, недопустимый переход already_exists, camera_limit, organization_deleting, camera_changed_retry
422 JSON корректен, но не соответствует схеме: не тот тип поля, нет обязательного поля, неизвестное значение перечисления тело формирует HTTP-слой, поля error в нём может не быть
429 слишком часто: блокировка входа по IP либо исчерпаны слоты потоковых подписок throttled, live_stream_capacity
500 внутренняя ошибка, которой быть не должно url_build_failed
501 такого маршрута нет — ответ на опечатку в пути not_implemented
502 отказ вышестоящей стороны: камера по ONVIF или нода onvif_upstream, node_bad_reply
503 временная деградация: недоступна база, нода или журнал аудита db_unavailable, node_unavailable, audit_unavailable

Как это читать на практике:

  • 404 — это «не найдено или не ваше». На маршрутах доступа к видео камера чужой организации отвечает 404 not_found, а не 403: Plane не раскрывает даже факт существования чужого объекта. На административных маршрутах чужая организация даёт 403 cross_org, потому что там объект уже опознан по идентификатору.
  • 409 camera_changed_retry — не ошибка, а гонка. Строку изменили параллельно, повторите запрос как есть.
  • 501, а не 404, на неизвестный путь. Если вы получили {"error":"not_implemented"} — ищите опечатку в URL, а не отсутствующий объект.
  • 503 — повторяйте с нарастающей паузой. Plane при отказе базы деградирует, но не падает; через несколько секунд запрос обычно проходит.
  • Нарушение уникальности (одинаковое имя организации, тега, шаблона, логина) всегда приходит как 409 already_exists.
  • Не всякая ошибка приходит в конверте {"error": …}. Запрос, который не удалось даже разобрать, отклоняет HTTP-слой до обработчика: битый JSON или неверный тип поля в теле дают 400/422, а негодная строка запроса — 400 с простым текстом Failed to deserialize query string …. Клиент должен переживать ответ, не являющийся JSON.

Постраничная выдача#

Списки не используют OFFSET и не считают общее количество строк — на установках с десятками тысяч камер это дало бы линейную деградацию. Вместо этого применяется keyset-пагинация: клиент передаёт позицию последней увиденной строки.

Форматов ответа три; какой у конкретного маршрута — указано в его описании.

Конверт {rows, next} — административные списки#

Так отвечают /api/v1/orgs, /api/v1/users, /api/v1/tags, /api/v1/cameras, /api/v1/camera-templates.

{
  "rows": [ { "id": "<UUID>", "…": "…" } ],
  "next": "<UUID>"
}
Параметр Тип По умолчанию Смысл
limit целое 500 сколько строк вернуть; значение прижимается к диапазону [1, 500]
after uuid вернуть строки со id > after; сюда подставляется next предыдущей страницы

Правила, которые стоит запомнить:

  • Сортировка — по возрастанию id. Идентификаторы монотонны по времени создания, поэтому это практически порядок добавления.
  • next — идентификатор последней строки страницы. Он заполняется только если страница вернулась ровно полной. Когда next равен null, страниц больше нет.
  • next ≠ «дальше точно что-то есть». Если общее число строк кратно limit, последняя страница придёт полной, next будет непустым, а следующий запрос вернёт {"rows":[],"next":null}. Это нормально: цикл завершается по next == null, а не по пустому ответу.
  • limit=0, отрицательный или отсутствующий limit дают полную страницу в 500 строк, а не пустой ответ и не маленькое значение по умолчанию.

Обход всех страниц целиком:

after=""
while :; do
  page=$(curl -sS "$BASE/api/v1/cameras?limit=500${after:+&after=$after}" \
           -H "Authorization: Bearer $TOKEN")

  # полезная работа со страницей: здесь — id и имя каждой камеры
  echo "$page" | python3 -c '
import sys, json
for r in json.load(sys.stdin)["rows"]:
    print(r["id"], r["name"], sep="\t")'

  after=$(echo "$page" | python3 -c 'import sys,json;print(json.load(sys.stdin)["next"] or "")')
  [ -n "$after" ] || break        # next == null → страниц больше нет
done

Конверт {…, next_cursor} — непрозрачный курсор#

Так отвечают маршруты, где сортировка идёт не по id, а по имени, или где выдача собирается с нескольких нод: /api/v1/sessions, /api/v1/mobile/cameras.

{ "rows": [  ], "next_cursor": "<непрозрачная строка>" }
  • Массив строк называется rows у /api/v1/mobile/cameras и items у /api/v1/sessions — последний отдаёт ещё limit, partial_nodes и sampled_at (unix-секунды).
  • Курсор передаётся обратно как ?cursor=…. Его не нужно разбирать — формат внутренний и может измениться.
  • limit здесь другой: по умолчанию 50, прижимается к [1, 100].
  • У /api/v1/mobile/cameras next_cursor — надёжный признак конца: он появляется, только если следующая строка действительно существует. Испорченный курсор там даёт 400 bad_cursor.
  • У /api/v1/sessions страница может прийти короче limit и всё равно иметь next_cursor — выдача собирается опросом нод (не более 4 нод за запрос, по 2 секунды на каждую). Испорченный курсор там, наоборот, молча игнорируется и выдача начинается сначала. Непустой partial_nodes в ответе означает «страница неполная», а не «ошибка».

Без пагинации#

Часть маршрутов возвращает заведомо ограниченный набор и курсора не имеет вовсе: GET /api/v1/cameras/lite (до 500 строк {id, name}), GET /api/v1/cameras/{id}/shares (действующие ссылки одной камеры). Ответ — {"rows":[…]} или {"shares":[…]} без поля next.

Потоки событий (SSE)#

Живое состояние отдаётся потоками Server-Sent Events — обычный HTTP-ответ Content-Type: text/event-stream, который не закрывается. Так работает, например, GET /api/v1/cameras/runtime/stream (статус камер в реальном времени).

Чтение из командной строки:

curl -N -sS "$BASE/api/v1/cameras/runtime/stream?ids=<UUID>,<UUID>" \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Accept: text/event-stream'

Ключ -N отключает буферизацию curl; без него кадры будут копиться и приходить пачками.

Как выглядит поток:

: keep-alive

id: 1
data: {"seq":1,"sampled_at":"2026-08-18T10:00:00Z","rows":{"<UUID>":{"alive":true,"health":"ok"}},"profiles":{}}

id: 2
data: {"seq":2,"sampled_at":"2026-08-18T10:00:01Z","rows":{"<UUID>":{"alive":true,"health":"ok"}},"profiles":{}}

Правила разбора — одинаковые для всех потоков этого API:

  • Имени события нет. Кадры приходят как события по умолчанию, поэтому в браузере их принимает source.onmessage, а не подписка на именованное событие.
  • id: — порядковый номер кадра (seq продублирован и внутри JSON).
  • Строка, начинающаяся с : — комментарий поддержания соединения; он приходит каждые 15 секунд и его нужно молча пропускать. Отсутствие таких строк дольше ~30 секунд — признак, что соединение порвал промежуточный прокси.
  • Каждый кадр — полное состояние на свой момент времени, а не приращение. Пропущенный или устаревший кадр не нужно догонять: следующий приведёт картину в порядок. При переподключении докачивать историю тоже не нужно.
  • Поток конечен. Он завершится не позже, чем истечёт актуальность авторизации вызывающего (не более 5 минут). Долгоживущая страница обязана переподключаться — считайте штатное закрытие потока нормой, а не ошибкой.
  • Слот подписки удерживается всё время, пока открыто тело ответа. Клиент, который открыл поток и не читает его, продолжает занимать слот. Закрывайте соединение явно.
  • При исчерпании слотов приходит 429 с телом {"error":"live_stream_capacity","scope":"global"|"principal","retry_after":N} и заголовком Retry-After. Дождитесь указанного времени, а не повторяйте сразу.
Реверс-прокси должен пропускать поток без буферизации

Если перед Plane стоит прокси, для маршрутов */stream отключите буферизацию ответа и увеличьте таймаут чтения. Иначе поток внешне «работает», но кадры приходят слипшимися пачками или соединение рвётся по таймауту через минуту.

Частые ошибки интеграции#

Ниже — то, на чём чаще всего теряют время при первой интеграции. Каждый пункт — реальный случай.

Пустое значение параметра — это не «параметр не задан»#

Правило: параметр либо передаётся с корректным значением, либо не передаётся совсем. Пустая строка в строке запроса — всегда ошибка. Пустое значение не равносильно отсутствию параметра ни для одного фильтра.

Отказ выглядит по-разному в зависимости от типа параметра:

# Строковый фильтр со списком допустимых значений: пустая строка в список не входит.
curl -sS "$BASE/api/v1/cameras?status=" -H "Authorization: Bearer $TOKEN"
# 400 {"error":"bad_camera_status"}

# Нестроковый параметр (число, uuid, bool): разбор строки запроса падает ДО обработчика,
# и ответ приходит обычным текстом, а не в конверте {"error": …}.
curl -sS "$BASE/api/v1/cameras?limit=" -H "Authorization: Bearer $TOKEN"
# 400 Failed to deserialize query string …

# ПРАВИЛЬНО: фильтр не нужен — параметра не должно быть в строке запроса вовсе.
curl -sS "$BASE/api/v1/cameras" -H "Authorization: Bearer $TOKEN"

Так же ведут себя ?org=, ?after=, ?tag=, ?node=, ?disabled= и любые другие параметры, тип которых не строка: пустое значение даёт текстовую ошибку 400 Failed to deserialize query string. Обработчик тела ошибки, рассчитанный на JSON, на такой ответ споткнётся — предусмотрите это в клиенте.

Собирайте строку запроса из непустых пар, а не подставляйте переменные в готовый шаблон:

q=""; status="live"; limit=100            # часть фильтров пустая
args=""
[ -n "$q" ]      && args="$args&q=$q"
[ -n "$status" ] && args="$args&status=$status"
[ -n "$limit" ]  && args="$args&limit=$limit"
curl -sS "$BASE/api/v1/cameras?${args#&}" -H "Authorization: Bearer $TOKEN"
Пустая выдача — это другое

{"rows":[],"next":null} — нормальный ответ на корректный фильтр, под который ничего не подошло: например, ?status=parked, когда припаркованных камер нет. Пустой результат и отказ разбора различаются кодом ответа: 200 против 400.

Всегда задавайте limit явно#

Отсутствующий, нулевой или отрицательный limit в конверте {rows, next} не даёт «разумное значение по умолчанию» — он даёт полную страницу в 500 записей. Запрос ?limit=0, которым обычно хотят «просто проверить доступность», выкачает 500 камер со всеми полями. Указывайте limit в каждом запросе списка.

Токен ссылки общего доступа выдаётся ровно один раз#

При создании ссылки (POST /api/v1/cameras/{id}/shares) токен возвращается только в ответе на создание. В базе хранится лишь идентификатор ссылки — он нужен для отзыва; сам токен прочитать повторно нельзя ни из списка ссылок, ни откуда-либо ещё. Потеряли — выпускайте новую ссылку и отзывайте старую.

Права ссылки — это пересечение двух ограничений: полномочий того, кто её выпускает, и физических возможностей самой камеры. Поэтому на камеру без записи невозможно выдать ссылку с доступом к архиву или экспорту, а на камеру без ONVIF — с PTZ, и суперадминистратор здесь не исключение. Запрошенные «лишние» биты не отбрасываются молча — запрос отклоняется с 403 bits_exceed_grant.

Интеграции нужна собственная учётная запись#

Организация может ограничивать число одновременных сессий одного пользователя (max_sessions_per_user). Когда лимит задан, новый вход вытесняет самые старые сессии этого же пользователя — побеждает свежий. Если интеграция логинится под тем же admin, что и живой человек, то каждый её запуск по расписанию будет молча выкидывать администратора из браузера (и наоборот — вход администратора будет ломать интеграцию, которая начнёт получать 401 evicted).

Никогда не используйте человеческий логин администратора в скриптах

Заведите для каждой интеграции отдельного пользователя с говорящим именем (integration-1c, backup-export, monitoring), выдайте ему минимально необходимую роль или гранты и держите пароль в хранилище секретов. Тогда:

  • вытеснение сессий никого не задевает;
  • в журнале аудита видно, кто именно совершил действие;
  • отключение интеграции — это отключение одной учётной записи, а не смена общего пароля.

Дополнительно: долгоживущему процессу вместо повторных входов следует продлевать сессию через POST /api/v1/session/refresh — тогда сессия остаётся одна и вытеснять нечего.

Смена пароля рвёт все сессии этого пользователя#

POST /api/v1/users/{id}/password отзывает все живые сессии владельца пароля, включая ту, из которой вы сделали запрос. Скрипт, который меняет свой же пароль и продолжает работу со старым токеном, получит 401 на следующем запросе. Выполните вход заново сразу после смены.

Символы % и _ в поиске работают как шаблон#

В поиске ?q= по административным спискам пользователей и камер эти символы не экранируются и ведут себя как подстановочные знаки SQL: ?q=% найдёт всё, ?q=a_c найдёт abc. Если вы подставляете в q текст, введённый пользователем, либо экранируйте эти символы на своей стороне, либо учитывайте эффект. В мобильной витрине и в GET /api/v1/cameras/lite они, наоборот, трактуются буквально.

Пропущенное поле и null — разные вещи#

В запросах PATCH пропуск поля означает «не трогать», а явный null — «очистить значение». Но очистить через null можно не любое поле: у части полей null равносилен пропуску. Какие именно поля очищаются, указано в описании каждого маршрута — например, для камер это перечислено в разделе Камеры.

И отдельно: null в числовых лимитах организации означает «без ограничения», а не «ноль». Ноль — это честный ноль, то есть «нельзя ничего».

Не запрашивайте секреты на каждой отрисовке#

GET /api/v1/cameras/{id}/credentials — единственный маршрут, отдающий пароль камеры, и каждый его вызов пишется в журнал аудита. Это действие «по нажатию кнопки». Интеграция, которая тянет пароли пачкой при каждом обновлении экрана, за сутки создаёт тысячи записей аудита и делает журнал бесполезным.

Проверяйте partial_nodes и next, а не длину массива#

Пустой массив в ответе не всегда означает «данных нет»: у списка сессий это может быть страница, собранная с ноды, которая не ответила (см. partial_nodes), а у конверта {rows, next} — обычная последняя страница цикла. Ориентируйтесь на признаки конца, описанные выше, а не на количество строк.

Не полагайтесь на поля-подсказки#

Флаги вроде ptz, events, live в дескрипторах просмотра — это подсказки для интерфейса («показывать ли кнопку»). Реальную проверку прав делает каждый маршрут самостоятельно, и нода проверяет их ещё раз. Интеграция, которая по подсказке решила, что действие разрешено, всё равно получит 403 — обрабатывайте его как штатный ответ.