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выдает преподавателю серверный grantModerator;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;- для камеры, микрофона и демонстрации нужны
allowpermissions на 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