Skip to content

Integration API v1

Integration API предназначен для backend-to-backend сценариев: внешняя платформа создает комнату по своему external_id, получает ссылку на hosted VideoGrace room и показывает ее пользователю в личном кабинете. На первом этапе внешний сервис не встраивает видеоклиент и не синхронизирует пользователей.

Готовность MVP

Текущий MVP уже можно использовать для пилотной интеграции, если сценарий выглядит так:

  • backend партнера создает или получает комнату по external_id урока;
  • VideoGrace возвращает hosted-ссылки для преподавателя и ученика;
  • партнер показывает ссылку пользователю в своем личном кабинете;
  • пользователь открывает VideoGrace web room в браузере;
  • партнер проверяет состояние комнаты polling'ом.

Что входит в v1:

  • service-account авторизация backend-to-backend;
  • server_info для проверки доступности API;
  • идемпотентное создание комнаты по external_id;
  • получение обычной ссылки на комнату;
  • отдельные signed join links для ролей teacher и student;
  • обновление названия и времени;
  • отмена, закрытие и status polling;
  • самодостаточный smoke-тест без внешних Python-зависимостей.

Что не входит в v1:

  • встраивание VideoGrace клиента в iframe или SDK;
  • user provisioning партнерских пользователей;
  • webhooks room.started, participant.joined, participant.left, room.ended;
  • управление правами отдельных участников после входа;
  • end-to-end браузерный тест открытия /j/<token> из smoke-теста.

Архив завершенных recording jobs доступен через отдельный Records API. Для связи урока и записи партнер должен сохранить tag, возвращенный Rooms API: в архиве этот же идентификатор находится в поле conference_tag. Запуск recorder и консолидация дорожек не входят в контракт Rooms API.

Базовый путь:

https://<server>/api/v1/integration

Включение модуля

Integration API является опциональным серверным модулем и по умолчанию отключен. Его данные не хранятся в main.db.

[Db]
IntegrationDb = /opt/VideoGrace/Server/db/integration.db

[Integration]
Enabled = 1
PublicUrl = https://join.example.com

Те же параметры можно задать переменными окружения VG_INTEGRATION_ENABLED=1, VG_INTEGRATION_DB=/path/integration.db и VG_SERVER_URL=https://join.example.com. Настройки применяются после перезапуска сервера.

При Enabled = 0 сервер не открывает и не создает integration.db, endpoint-ы /api/v1/integration/* возвращают HTTP 404, а signed partner policy, webinar layout и автоматическая запись не применяются к обычным конференциям.

При первом включении создается чистая БД с таблицами service_accounts и rooms. Автоматического импорта старой таблицы integration_rooms из main.db нет.

Отключенный режим можно проверить без service account:

python3 tests/integration/vg_integration_api_smoke.py \
  --base-url https://join.example.com \
  --expect-disabled

Типовые сценарии

Сценарий 1. Создание урока в LMS

Партнерская платформа создает урок у себя и вызывает:

PUT /api/v1/integration/rooms/notabridge_lesson_987
Authorization: Bearer <access_token>
Content-Type: application/json

{
  "title": "Урок фортепиано",
  "starts_at": "2026-07-10T15:00:00+07:00",
  "ends_at": "2026-07-10T15:45:00+07:00"
}

VideoGrace создает конференцию и возвращает:

  • join_url - общая ссылка на комнату;
  • teacher_join_url - ссылка преподавателя;
  • student_join_url - ссылка ученика.

Партнер сохраняет room_id, tag и ссылки у себя рядом с уроком.

Сценарий 2. Повторная отправка после сетевого сбоя

Если PUT /rooms/{external_id} был отправлен, но ответ не был получен из-за таймаута, партнер должен повторить тот же PUT с тем же external_id.

Метод идемпотентный:

  • если комнаты еще нет, она будет создана;
  • если комната уже есть, сервер вернет существующую комнату;
  • повторный PUT не перезаписывает title, starts_at, ends_at.

Для изменения уже созданной комнаты используйте PATCH.

Сценарий 3. Перенос урока

Когда урок перенесли в LMS:

PATCH /api/v1/integration/rooms/notabridge_lesson_987
Authorization: Bearer <access_token>
Content-Type: application/json

{
  "title": "Урок фортепиано: новое время",
  "starts_at": "2026-07-10T16:00:00+07:00",
  "ends_at": "2026-07-10T16:45:00+07:00"
}

tag и ссылки не меняются. Пользователь может открыть прежнюю ссылку.

Сценарий 4. Отмена урока

Если урок отменен до начала:

POST /api/v1/integration/rooms/notabridge_lesson_987/cancel
Authorization: Bearer <access_token>

Room получает статус cancelled. Партнеру следует скрыть ссылки или показать пользователю состояние "урок отменен".

Сценарий 5. Завершение урока

Если платформа принудительно завершает урок:

POST /api/v1/integration/rooms/notabridge_lesson_987/close
Authorization: Bearer <access_token>

Room получает статус closed. Если конференция активна, сервер пытается завершить ее.

Сценарий 6. Проверка состояния перед показом кнопки "Войти"

Партнер может регулярно выполнять:

GET /api/v1/integration/rooms/notabridge_lesson_987/status
Authorization: Bearer <access_token>

Рекомендуемая логика:

  • status=scheduled - показать кнопку "Войти" или "Ожидает начала";
  • status=active и live=true - урок идет;
  • status=cancelled - не показывать ссылку входа;
  • status=closed - показать "урок завершен".

Поток подключения

sequenceDiagram
    participant LMS as Partner backend
    participant VG as VideoGrace Integration API
    participant T as Teacher browser
    participant S as Student browser

    LMS->>VG: POST /auth/token
    VG-->>LMS: access_token
    LMS->>VG: PUT /rooms/{external_id}
    VG-->>LMS: room_id, tag, teacher_join_url, student_join_url
    LMS-->>T: показать teacher_join_url
    LMS-->>S: показать student_join_url
    T->>VG: открыть /j/<teacher_token>
    S->>VG: открыть /j/<student_token>
    LMS->>VG: GET /rooms/{external_id}/status
    VG-->>LMS: scheduled | active | cancelled | closed

Авторизация

Внешний backend получает короткий bearer token через service account:

POST /api/v1/integration/auth/token
Content-Type: application/json

{
  "service_account": "notabridge",
  "client_secret": "..."
}

Ответ:

{
  "result": 200,
  "access_token": "...",
  "token_type": "Bearer",
  "expires_in": 604800,
  "refresh_token": null
}

refresh_token в v1 не выдается. Для нового access token backend повторяет auth/token.

Все остальные методы вызываются с заголовком:

Authorization: Bearer <access_token>

Настройка service account

Service account привязывается к обычному пользователю VideoGrace. Этот пользователь становится владельцем созданных интеграцией конференций.

Для партнера создавайте отдельного технического пользователя, а не привязывайте service account к администратору. Так проще ограничить права, аудит и последующую ротацию секрета.

Сначала создайте технического пользователя в основной БД:

insert into clients
  (name, login, password, type, number, time_limit, grants, creation_time, guid)
values
  ('NotaBridge Service', 'notabridge_service', '', 1, 900001, 0, 0, strftime('%s','now'), lower(hex(randomblob(16))));

После запуска сервера с Integration.Enabled = 1 добавьте партнерский account уже в отдельную integration.db. Для тестового стенда можно использовать plain-secret:

insert into service_accounts
  (name, client_id, secret_hash, scopes, enabled, created_at)
values
  ('notabridge', 228800123, 'plain:dev-secret', '["integration:rooms"]', 1, strftime('%s','now'))
on conflict(name) do update set
  client_id = excluded.client_id,
  secret_hash = excluded.secret_hash,
  scopes = excluded.scopes,
  enabled = 1;

Здесь 228800123 - clients.id созданного технического пользователя из main.db. Например:

CLIENT_ID="$(sqlite3 /opt/VideoGrace/Server/db/main.db \
  "select id from clients where login='notabridge_service'")"

sqlite3 /opt/VideoGrace/Server/db/integration.db \
  "insert into service_accounts(name, client_id, secret_hash, scopes, enabled, created_at)
   values('notabridge', $CLIENT_ID, 'plain:dev-secret', '[\"integration:rooms\"]', 1, strftime('%s','now'))
   on conflict(name) do update set client_id=excluded.client_id,
     secret_hash=excluded.secret_hash, scopes=excluded.scopes, enabled=1"

Для production храните не сам секрет, а HMAC от него:

secret_hash = hmac-sha256:<base64url(hmac_sha256(VG_JWT_SECRET, client_secret))>

Хеш можно посчитать поставочным smoke-тестом:

VG_JWT_SECRET='server-jwt-secret' \
VG_INTEGRATION_CLIENT_SECRET='partner-client-secret' \
python3 tests/integration/vg_integration_api_smoke.py --print-secret-hash

После создания нового пользователя нужен рестарт vgserver, потому список пользователей загружается сервером в память. Изменение записи partner account в integration.db рестарта не требует.

Проверка API

GET /api/v1/integration/server_info
Authorization: Bearer <access_token>

Ответ содержит версию и advertised features:

{
  "result": 200,
  "api": {
    "version": "v1",
    "base_path": "/api/v1/integration",
    "features": [
      "rooms.idempotent_put",
      "rooms.status_polling",
      "signed_join_links",
      "signed_join_links.resolve",
      "signed_join_links.display_name",
      "signed_join_links.teacher_moderator",
      "rooms.partner_videoroom",
      "rooms.automatic_recording"
    ]
  },
  "server": {}
}

Комнаты

external_id задает внешний идентификатор урока. Он должен быть непустым и не длиннее 255 символов.

Рекомендуемый формат:

<partner>_<entity>_<id>
notabridge_lesson_987

external_id должен быть стабильным. Не используйте в нем случайный UUID на каждый retry, иначе идемпотентность потеряется.

Idempotent create/get

PUT /api/v1/integration/rooms/notabridge_lesson_987
Authorization: Bearer <access_token>
Content-Type: application/json

{
  "title": "Урок фортепиано",
  "starts_at": "2026-07-10T15:00:00+07:00",
  "ends_at": "2026-07-10T15:45:00+07:00",
  "description": "Индивидуальный урок",
  "teacher": {
    "display_name": "Иван Петров"
  },
  "student": {
    "display_name": "Мария Иванова"
  }
}

Если комнаты еще нет, сервер создает конференцию и возвращает ссылки. Если external_id уже существует, сервер возвращает существующую комнату и не перезаписывает ее поля.

{
  "room_id": 123,
  "external_id": "notabridge_lesson_987",
  "tag": "lesson-a8f3k2",
  "title": "Урок фортепиано",
  "starts_at": "2026-07-10T15:00:00+07:00",
  "ends_at": "2026-07-10T15:45:00+07:00",
  "status": "scheduled",
  "join_url": "https://join.videograce.ru/conferences/lesson-a8f3k2",
  "teacher_join_url": "https://join.videograce.ru/j/...",
  "student_join_url": "https://join.videograce.ru/j/...",
  "teacher": {
    "display_name": "Иван Петров"
  },
  "student": {
    "display_name": "Мария Иванова"
  }
}

Получение существующей комнаты:

GET /api/v1/integration/rooms/notabridge_lesson_987
Authorization: Bearer <access_token>

Практический контракт PUT:

  • external_id - обязательный path parameter;
  • title или name - человекочитаемое название комнаты;
  • starts_at и ends_at - ISO 8601 строка с timezone;
  • description - опционально;
  • teacher.display_name - опциональное имя преподавателя для hosted join page;
  • student.display_name - опциональное имя ученика для hosted join page;
  • ответ всегда содержит актуальные ссылки.

Signed join resolver

Hosted web-client открывает teacher_join_url и student_join_url как обычные страницы вида /j/<token>. Под капотом клиент вызывает публичный resolver:

GET /api/v1/integration/join/<token>

Bearer token здесь не нужен: сам <token> является секретом signed link. Ответ не содержит teacher_join_url и student_join_url, чтобы публичный resolver не раскрывал вторую роль.

{
  "result": 200,
  "role": "teacher",
  "display_name": "Иван Петров",
  "room_id": 123,
  "external_id": "notabridge_lesson_987",
  "tag": "lesson-a8f3k2",
  "title": "Урок фортепиано",
  "starts_at": "2026-07-10T15:00:00+07:00",
  "ends_at": "2026-07-10T15:45:00+07:00",
  "status": "scheduled",
  "join_url": "https://join.videograce.ru/conferences/lesson-a8f3k2",
  "ui_profile": "partner_videoroom",
  "layout_mode": "webinar",
  "features": {
    "main_navigation": false,
    "chat": false,
    "transcription": false,
    "recording_controls": false
  },
  "recording": {
    "mode": "automatic"
  }
}

partner_videoroom является фиксированным hosted-профилем интеграции. У преподавателя и ученика отсутствуют главное меню VideoGrace, чат комнаты, расшифровка, управление транскрибером и ручные кнопки старта/остановки записи. Остаются сама видеокомната и ее media controls. Комната создается с раскладкой webinar по умолчанию. Если подключены только преподаватель и ученик, VAD продолжает подсвечивать говорящего, но не меняет плитки местами; динамический выбор докладчиков начинается с трех участников.

Signed token передается web-клиентом в control-команде входа и проверяется сервером для конкретного tag. Роль нельзя повысить изменением query string или состояния браузера:

  • teacher_join_url выдает преподавателю серверный grant Moderator;
  • student_join_url подключает обычного участника;
  • токен одной комнаты не действует в другой комнате;
  • после F5 и control WebSocket reconnect токен хранится только в sessionStorage текущей вкладки и повторно передается серверу.

Автоматическая запись

При первом подключении участника к партнерской комнате сервер идемпотентно ставит CAN-задачу recording.start с профилем scope=partner_auto. Запись включает звук, видео и демонстрацию экрана и загружается в server storage. Одновременный вход преподавателя и ученика не создает две активные задачи записи для одной комнаты.

Recorder worker должен быть запущен и зарегистрирован с ролью recorder и capability recording. Если worker недоступен или service account recorder не настроен, вход пользователей не блокируется, а причина отказа запуска записи фиксируется в серверном логе. В hosted UI ручное управление записью намеренно отсутствует; активная запись обозначается индикатором REC.

Update

PATCH обновляет изменяемые параметры комнаты:

PATCH /api/v1/integration/rooms/notabridge_lesson_987
Authorization: Bearer <access_token>
Content-Type: application/json

{
  "title": "Урок фортепиано: перенос времени",
  "starts_at": "2026-07-10T16:00:00+07:00",
  "ends_at": "2026-07-10T16:45:00+07:00",
  "teacher": {
    "display_name": "Иван Петров"
  },
  "student": {
    "display_name": "Мария Иванова"
  }
}

PATCH можно вызывать с частичным body. Поля, которых нет в body, остаются без изменений.

Cancel и close

Отмена деактивирует конференцию и переводит room в cancelled:

POST /api/v1/integration/rooms/notabridge_lesson_987/cancel
Authorization: Bearer <access_token>

Закрытие завершает активную конференцию и переводит room в closed:

POST /api/v1/integration/rooms/notabridge_lesson_987/close
Authorization: Bearer <access_token>

Status polling

GET /api/v1/integration/rooms/notabridge_lesson_987/status
Authorization: Bearer <access_token>

Ответ совпадает с room response и дополнительно содержит:

{
  "status": "active",
  "stored_status": "scheduled",
  "live": true
}

status=active вычисляется динамически, когда конференция сейчас активна, а сохраненный статус еще scheduled.

Ошибки и retry policy

Ответы API возвращаются как JSON-объект. При ошибке обычно используются поля:

{
  "result": 401,
  "message": "Unauthorized"
}

Рекомендуемая обработка:

HTTP/result Причина Что делать
400 неверный JSON, пустой external_id, ошибка сохранения исправить payload, retry тем же body не поможет
401 неверный bearer token или client secret запросить новый token; если не помогло, проверить service account
404 room по external_id не найден создать через PUT или проверить внешний id
5xx временная серверная ошибка повторить с backoff; для create использовать тот же external_id
network timeout неизвестно, выполнен ли запрос для PUT повторить тот же external_id; для PATCH/cancel/close можно безопасно повторить

Рекомендуемый backoff для backend-to-backend вызовов:

  • 1 секунда;
  • 3 секунды;
  • 10 секунд;
  • далее ручная диагностика или фоновая очередь retry.

Не делайте бесконечный tight loop. Все операции должны логироваться у партнера с external_id, HTTP status и room_id/tag, если они уже известны.

Роли и ссылки

API возвращает три ссылки:

  • join_url - обычная ссылка на комнату.
  • teacher_join_url - signed join link преподавателя.
  • student_join_url - signed join link ученика.

Модель signed join links выбрана для MVP, чтобы NotaBridge не синхронизировал пользователей на первом этапе. Расширение с user provisioning можно добавить отдельными методами позже.

Преподаватель по teacher_join_url является модератором только в рамках текущего подключения по подписанному токену. Модераторская роль не записывается как постоянное право технического гостевого аккаунта.

Backend API выдает токены и ссылки как часть контракта. End-to-end вход по https://<server>/j/<token> поддерживается hosted web-client через публичный resolver /api/v1/integration/join/<token>. Smoke-тест ниже проверяет выдачу ссылок и resolver, но не открывает браузер и не подтверждает пользовательский prejoin.

Если в room передан teacher.display_name или student.display_name, resolver возвращает это имя в поле display_name, а hosted web-client подставляет его в prejoin форму. Пользователь может изменить имя перед входом. В текущем MVP имя хранится в room metadata и не требует user provisioning.

Iframe embedding

/j/<token> технически является hosted web-client страницей и может открываться в iframe, если deployment разрешает встраивание через CSP. Для production это нужно явно настроить на стороне VideoGrace/proxy:

Content-Security-Policy: frame-ancestors 'self' https://<partner-domain>

Минимальный iframe:

<iframe
  src="https://<server>/j/<token>"
  allow="camera; microphone; autoplay; fullscreen; display-capture"
  allowfullscreen
></iframe>

Ограничения v1:

  • если CSP frame-ancestors не разрешает домен партнера, браузер заблокирует iframe;
  • X-Frame-Options: DENY/SAMEORIGIN нельзя использовать вместе с cross-origin embedding;
  • для камеры, микрофона и демонстрации нужны allow permissions на iframe;
  • браузерные политики autoplay и permissions могут отличаться между Chrome/Safari/Firefox, поэтому embedded режим требует отдельного приемочного теста у партнера;
  • display name передается через signed link metadata, а не query string.

Рекомендуемая логика в личном кабинете:

  • преподавателю показывать только teacher_join_url;
  • ученику показывать только student_join_url;
  • join_url использовать как fallback или админскую ссылку;
  • не отправлять signed links в публичные логи и analytics;
  • при компрометации ссылки закрыть комнату и создать новую с новым external_id или будущим endpoint ротации ссылок.

Минимальный пример backend-клиента

BASE_URL="https://stage1.videograce.ru"
SERVICE_ACCOUNT="notabridge"
CLIENT_SECRET="dev-secret"
EXTERNAL_ID="notabridge_lesson_987"

TOKEN="$(
  curl -sS "$BASE_URL/api/v1/integration/auth/token" \
    -H 'Content-Type: application/json' \
    -d "{\"service_account\":\"$SERVICE_ACCOUNT\",\"client_secret\":\"$CLIENT_SECRET\"}" |
  python3 -c 'import json,sys; print(json.load(sys.stdin)["access_token"])'
)"

curl -sS -X PUT "$BASE_URL/api/v1/integration/rooms/$EXTERNAL_ID" \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
    "title": "Урок фортепиано",
    "starts_at": "2026-07-10T15:00:00+07:00",
    "ends_at": "2026-07-10T15:45:00+07:00"
  }'

Интеграционный smoke-тест

В репозитории есть самодостаточный тест без внешних Python-зависимостей:

VG_INTEGRATION_BASE_URL=https://join.videograce.ru \
VG_INTEGRATION_SERVICE_ACCOUNT=notabridge \
VG_INTEGRATION_CLIENT_SECRET='partner-client-secret' \
python3 tests/integration/vg_integration_api_smoke.py

Для локального сервера с self-signed TLS:

VG_INTEGRATION_BASE_URL=https://localhost:9443 \
VG_INTEGRATION_SERVICE_ACCOUNT=notabridge \
VG_INTEGRATION_CLIENT_SECRET='dev-secret' \
VG_INTEGRATION_INSECURE_TLS=1 \
python3 tests/integration/vg_integration_api_smoke.py

Сценарий проверяет:

  • выпуск service bearer token;
  • server_info и advertised features;
  • создание комнаты через PUT;
  • идемпотентность повторного PUT;
  • чтение комнаты через GET;
  • обновление через PATCH;
  • polling статуса;
  • cancel;
  • close;
  • финальный статус closed.

Тест создает комнату с external_id=vg-smoke-<utc timestamp> и в конце закрывает ее. Для повторяемого прогона задайте свой VG_INTEGRATION_EXTERNAL_ID.

Пример успешного прогона:

PASS auth/token
PASS server_info
PASS rooms PUT create external_id=vg-smoke-20260715T174001Z tag=lesson-cbcac7b3
PASS rooms PUT idempotent
PASS rooms GET
PASS rooms PATCH
PASS rooms status status=scheduled live=False
PASS rooms cancel
PASS rooms close
PASS rooms final status
PASS integration API smoke test