Integration API v1
Практическая инструкция с примерами запросов: кнопка видеовстречи в CRM и сохранение расшифровки.
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;
- подписка на webhook
recording.readyпосле завершения загрузки записи; - подписка на webhook
transcription.readyс итогом живой транскрипции одного CAN job; - самодостаточный 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, participants,
webhook_subscriptions и webhook_deliveries.
Автоматического импорта старой таблицы 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/partner_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/partner_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/partner_lesson_987/cancel
Authorization: Bearer <access_token>
Room получает статус cancelled. Партнеру следует скрыть ссылки или показать пользователю состояние "урок отменен".
Сценарий 5. Завершение урока
Если платформа принудительно завершает урок:
POST /api/v1/integration/rooms/partner_lesson_987/close
Authorization: Bearer <access_token>
Room получает статус closed. Если конференция активна, сервер пытается завершить ее.
Сценарий 6. Проверка состояния перед показом кнопки "Войти"
Партнер может регулярно выполнять:
GET /api/v1/integration/rooms/partner_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>
T->>VG: POST /integration/join/<teacher_token>
VG-->>T: токены постоянного аккаунта по teacher.external_id
S->>VG: POST /integration/join/<student_token>
VG-->>S: токены постоянного аккаунта по student.external_id
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": "partner",
"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
('Partner Service', 'partner_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
('partner', 228800123, 'plain:dev-secret', '["integration:rooms","blob"]', 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='partner_service'")"
sqlite3 /opt/VideoGrace/Server/db/integration.db \
"insert into service_accounts(name, client_id, secret_hash, scopes, enabled, created_at)
values('partner', $CLIENT_ID, 'plain:dev-secret', '[\"integration:rooms\",\"blob\"]', 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",
"webhooks.recording_ready",
"webhooks.transcription_ready"
]
},
"server": {}
}
Webhook готовности записи и расшифровки
Webhook настраивается один раз для service account и применяется ко всем
созданным этим техническим пользователем партнерским комнатам. На каждую
готовую запись VideoGrace отправляет событие recording.ready только после
того, как recorder завершил upload всех артефактов и сервер успешно сохранил
запись в records.db.
transcription.ready передает текст, который транскрибер распознавал во время
разговора. Он отправляется после завершения задания транскрибера, независимо
от записи и ее загрузки. Повторное распознавание MP4 не требуется.
Настройка
PUT /api/v1/integration/webhook
Authorization: Bearer <access_token>
Content-Type: application/json
{
"url": "https://partner.example.com/webhooks/videograce",
"secret": "replace-with-at-least-16-random-characters",
"events": ["recording.ready"],
"enabled": true
}
Требования:
- URL должен использовать публичный
https://endpoint; - redirect не обрабатывается, endpoint должен сразу вернуть
2xx; secretдолжен содержать от 16 до 256 символов;- при обновлении настройки существующие
url,secretиeventsможно не передавать; enabled=falseотключает новые доставки и отменяет ожидающие retry;- поддерживаются
recording.readyиtranscription.ready; без явного списка новая подписка включает толькоrecording.ready; - отправляются только выбранные события; удаление типа из подписки отменяет его ожидающие доставки при следующей обработке очереди.
Например, временно отключить уже настроенный webhook можно без повторной передачи секрета:
PUT /api/v1/integration/webhook
Authorization: Bearer <access_token>
Content-Type: application/json
{"enabled": false}
Ответ не возвращает секрет:
{
"result": 200,
"configured": true,
"url": "https://partner.example.com/webhooks/videograce",
"events": ["recording.ready"],
"enabled": true,
"secret_configured": true
}
Прочитать текущую настройку:
GET /api/v1/integration/webhook
Authorization: Bearer <access_token>
Payload transcription.ready
Для получения текста включите "events": ["transcription.ready"], либо оба
типа: "events": ["recording.ready", "transcription.ready"].
Новый сервер сообщает поддержку через feature webhooks.transcription_ready
в server_info. Обновить нужно также worker: старый транскрибер не отправляет
финальный CAN artifact.
{
"id": "wh_example_transcription",
"type": "transcription.ready",
"created_at": "2026-09-13T10:30:05Z",
"data": {
"room": {"external_id": "crm_meeting_987", "tag": "lesson-a8f3k2"},
"transcription": {
"id": "transcriber-job-001",
"job_id": "transcriber-job-001",
"conference_tag": "lesson-a8f3k2",
"conference_session_id": 1842,
"status": "completed",
"language": "ru",
"started_at_ms": 1789293600000,
"ended_at_ms": 1789295400000,
"reason": "empty_conference",
"text": "Обсудим предложение.\nДоговорились о пилоте.",
"segments": [
{"seq": 1, "at": 1789293605000, "speaker": {"client_id": 101, "device_id": 1000, "name": "Иван"}, "text": "Обсудим предложение.", "partial": false},
{"seq": 2, "at": 1789295395000, "speaker": {"client_id": 102, "device_id": 1001, "name": "Мария"}, "text": "Договорились о пилоте.", "partial": false}
]
}
}
}
- Это итог одного запуска транскрибера, а не событие
room.ended. При остановке и повторном включении в одном созвоне будут разныеjob_idпри одинаковомconference_session_id. textсодержит тексты сегментов, разделенные переводом строки; имена есть вsegments[].speaker.atобозначает время публикации распознанного фрагмента, а не точный таймкод слова в аудиозаписи.started_at_ms/ended_at_msописывают работу задания, включая завершение распознавания; они не заменяют статистику посещения встречи.- Пустой результат допустим:
text: "",segments: [], если речь не распознана. - Сервер принимает итог только от назначенного CAN-воркера;
job_id, комната и ID созвона сверяются с заданием. Доставка идет владельцу партнерской комнаты. - Подпись и повторы такие же, как для
recording.ready. Дедупликация доставки:event.id; хранения текста: сервер +conference_session_id+job_id. - Создание комнаты не запускает транскрибер автоматически. Запуск через CAN настраивается отдельно: Transcriber.
- Очередь webhook сохраняется в БД после приема финального события сервером. Аварийное завершение worker до отправки итога не считается успешной транскрипцией; отдельного API архива транскриптов пока нет.
Payload recording.ready
{
"id": "wh_52b39d1d41a540cea7a1d10d2830e694",
"type": "recording.ready",
"created_at": "2026-08-05T10:15:42Z",
"data": {
"room": {
"external_id": "partner_lesson_987",
"tag": "lesson-a8f3k2"
},
"recording": {
"id": "19fd13d97fd0",
"job_id": "19fd13d97fd0",
"conference_tag": "lesson-a8f3k2",
"conference_session_id": 1842,
"profile": "raw_tracks_v1",
"status": "stopped",
"started_at_ms": 1785924900000,
"ended_at_ms": 1785927600000,
"duration_ms": 2700000,
"report_blob_id": "7bd85ee0-...",
"report_url": "https://join.example.com/api/storage/blobs/7bd85ee0-...",
"manifest_blob_id": "42d62bd0-...",
"manifest_url": "https://join.example.com/api/storage/blobs/42d62bd0-...",
"files": []
}
}
}
data.room.external_id позволяет связать событие с уроком без отдельного
поиска по conference_tag. data.recording.conference_session_id однозначно
связывает запись с конкретным запуском комнаты, статистикой и расшифровкой.
Состав files и наличие отдельных report URL
зависят от профиля recorder. Дополнительные поля могут добавляться без смены
версии события.
Blob URL приватны. Для скачивания файла backend партнера должен получить актуальный Integration API access token и передать его в запросе:
GET /api/storage/blobs/{blob_id}
Authorization: Bearer <access_token>
Service account получает доступ только к артефактам комнат, владельцем которых
он является. Выдавать техническому пользователю глобальный grant
recordings.access для этого не требуется.
Для интеграции с выгрузкой записей у аккаунта в integration.db.service_accounts
нужны оба scope: integration:rooms для комнат и blob для скачивания.
blob не отменяет ACL: чужие приватные записи остаются недоступными.
Один integration:rooms позволяет получать webhook, но скачивание вернет 403
как с Bearer, так и с параметром t.
После добавления blob в существующий список scopes получите новый token
через POST /api/v1/integration/auth/token: уже выданный JWT сохраняет старые
scopes до истечения срока. Перезапуск vgserver и повторная запись урока не нужны;
URL из ранее полученных webhook также работают с новым token.
Предпочитайте заголовок Bearer: token в URL может попасть в историю и access-логи.
Открытие такого URL непосредственно в адресной строке браузера без заголовка
Authorization ожидаемо вернет HTTP 401.
Подпись
Каждая доставка содержит заголовки:
X-VideoGrace-Event: recording.ready
X-VideoGrace-Delivery: wh_52b39d1d41a540cea7a1d10d2830e694
X-VideoGrace-Timestamp: 1785924942
X-VideoGrace-Signature: sha256=<hex-hmac>
Подписывается точная последовательность байт:
<X-VideoGrace-Timestamp>.<raw HTTP body>
Пример проверки на Python:
import hashlib
import hmac
import time
def verify(headers, raw_body: bytes, secret: str) -> bool:
timestamp = headers["X-VideoGrace-Timestamp"]
if abs(time.time() - int(timestamp)) > 300:
return False
signed = timestamp.encode("ascii") + b"." + raw_body
expected = hmac.new(secret.encode("utf-8"), signed, hashlib.sha256).hexdigest()
supplied = headers["X-VideoGrace-Signature"].removeprefix("sha256=")
return hmac.compare_digest(expected, supplied)
Проверять подпись нужно до JSON parsing. Рекомендуется также отклонять timestamp старше пяти минут.
Повторы и идемпотентность
Получатель должен вернуть любой HTTP 2xx только после надежного сохранения
события. При сетевой ошибке, 408, 425, 429 или 5xx VideoGrace выполняет
до шести попыток с задержками 10, 30, 120, 600 и 1800 секунд.
Остальные ответы 4xx считаются окончательной ошибкой.
Повторные попытки используют одинаковый id и X-VideoGrace-Delivery, поэтому
получатель должен делать upsert или insert-if-absent по этому идентификатору.
Очередь хранится в integration.db и переживает рестарт VideoGrace Server.
Smoke-тест настройки webhook:
python3 tests/integration/vg_integration_api_smoke.py \
--base-url https://join.example.com \
--service-account partner \
--client-secret "$CLIENT_SECRET" \
--webhook-url https://partner.example.com/webhooks/videograce \
--webhook-secret "$WEBHOOK_SECRET"
Комнаты
external_id задает внешний идентификатор урока. Он должен быть непустым и не длиннее 255 символов.
Рекомендуемый формат:
<partner>_<entity>_<id>
partner_lesson_987
external_id должен быть стабильным. Не используйте в нем случайный UUID на каждый retry, иначе идемпотентность потеряется.
Idempotent create/get
PUT /api/v1/integration/rooms/partner_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": "partner_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://video.example.com/conferences/lesson-a8f3k2",
"teacher_join_url": "https://video.example.com/j/...",
"student_join_url": "https://video.example.com/j/...",
"teacher": {
"display_name": "Иван Петров"
},
"student": {
"display_name": "Мария Иванова"
}
}
Получение существующей комнаты:
GET /api/v1/integration/rooms/partner_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;teacher.external_id,student.external_id- стабильные ID людей в системе партнёра; строка от 1 до 255 байт. Передавайте их для повторного использования аккаунта между уроками и устройствами;- ответ всегда содержит актуальные ссылки.
Постоянные аккаунты участников
При создании комнаты передавайте идентификаторы людей, не идентификаторы урока:
{
"title": "Урок гитары",
"teacher": {"external_id": "teacher:42", "display_name": "Иван Петров"},
"student": {"external_id": "student:108", "display_name": "Мария Иванова"}
}
Ключ идентичности: технический пользователь партнёра (service_accounts.client_id) + внешний ID человека. ID урока, имя, роль и браузер в этот ключ не входят. Один преподаватель получает один аккаунт во всех уроках партнёра; совпадающие имена разных людей не объединяются. Если ID в таблицах преподавателей и учеников могут совпадать, используйте разные пространства имён (teacher:42, student:42). Если у вас единый ID человека для обеих ролей, передавайте его неизменно.
При participant_auth: true в ответе resolver web-клиент вызывает:
POST /api/v1/integration/join/<personal_token>
Сервер находит или создаёт обычный пользовательский аккаунт и возвращает result: 200, login, user_id, access_token, refresh_token, token_type: "Bearer". Это не service token: права модератора по-прежнему назначаются при входе в конкретный урок по преподавательской ссылке. Refresh token сохраняется клиентом до подключения WSS. Повтор запроса, новая вкладка, другое устройство или очищенное browser storage не создают нового человека. Привязка хранит client_id, поэтому смена логина аккаунта не теряет связь.
GET выдаёт только данные приглашения, не токены авторизации. Персональная ссылка теперь является ключом входа в аккаунт человека: отдавайте её только этому пользователю через авторизованный кабинет LMS, не публикуйте и не используйте одну преподавательскую ссылку для нескольких людей. Закрытые и отменённые уроки не выдают новые токены (403); уже выданные сессии живут по обычным правилам JWT. При изменении teacher.external_id или student.external_id через PATCH ссылка соответствующей роли ротируется; старый URL перестаёт работать. Партнёр обязан использовать новые URL из ответа.
Для уже созданных уроков добавьте ID через PATCH /rooms/{external_id}: повторный PUT намеренно не меняет существующую комнату. После добавления ID также используйте обновлённые ссылки. Миграция схемы integration.db выполняется сервером при запуске и не удаляет прежние данные. Старые дубли не объединяются по имени и не удаляются автоматически.
Без внешних ID остаётся гостевой режим: в одном браузерном профиле используется сохранённая гостевая сессия, независимо от ссылки и вкладки. Сбой подключения не удаляет её и не разрешает новую регистрацию; переход к следующей сессии допускается только после явного ответа об удалённом аккаунте. Для браузеров с Web Locks одновременная авторегистрация во вкладках сериализована. Без ID нельзя гарантировать единую личность между устройствами, после удаления browser storage или в приватном режиме: для этого партнёр должен передавать ID людей.
Проверка на изолированном сервере: python3 tests/integration/vg_participant_identity_smoke.py с переменными VG_INTEGRATION_BASE_URL, VG_INTEGRATION_SERVICE_ACCOUNT, VG_INTEGRATION_CLIENT_SECRET. Для проверки изоляции второго партнёра задайте VG_OTHER_SERVICE_ACCOUNT и VG_OTHER_CLIENT_SECRET. Тест создаёт пользователей и комнаты, проверяет параллельные входы, смену роли, ротацию ссылок, refresh и закрывает тестовые комнаты. Пользователей не удаляет.
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",
"participant_auth": true,
"display_name": "Иван Петров",
"room_id": 123,
"external_id": "partner_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://video.example.com/conferences/lesson-a8f3k2",
"ui_profile": "partner_videoroom",
"layout_mode": "webinar",
"features": {
"main_navigation": false,
"chat": true,
"transcription": false,
"recording_controls": false
},
"recording": {
"mode": "automatic"
}
}
partner_videoroom является фиксированным hosted-профилем интеграции. Преподавателю и ученику доступны видеокомната, управление медиа, чат комнаты и кнопка полноэкранного режима рядом с выбором раскладки. Чат использует обычные сообщения конференции и сохраняет ограничения модерации, включая режим «только чтение». Главное меню VideoGrace, отдельная панель расшифровки, управление транскрибером и ручные кнопки старта/остановки записи скрыты. Комната создается с раскладкой webinar по умолчанию. Если подключены только преподаватель и ученик, VAD продолжает подсвечивать говорящего, но не меняет плитки местами; динамический выбор докладчиков начинается с трех участников.
Полноэкранный режим сохраняет видеоэлементы и открытый чат без переподключения медиа. Если браузер не предоставляет Fullscreen API, комната разворачивается в пределах текущего окна; системные панели браузера при этом остаются. Для встроенной комнаты требуется разрешение fullscreen от родительского iframe, как в примере ниже.
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/partner_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/partner_lesson_987/cancel
Authorization: Bearer <access_token>
Закрытие завершает активную конференцию и переводит room в closed:
POST /api/v1/integration/rooms/partner_lesson_987/close
Authorization: Bearer <access_token>
Status polling
GET /api/v1/integration/rooms/partner_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, чтобы партнёрская система не синхронизировала пользователей на первом этапе. Расширение с 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 форму. Пользователь может изменить имя перед входом. Имя не является ключом идентичности: для постоянного аккаунта передавайте external_id человека, как описано выше.
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://video.example.com"
SERVICE_ACCOUNT="partner"
CLIENT_SECRET="dev-secret"
EXTERNAL_ID="partner_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://video.example.com \
VG_INTEGRATION_SERVICE_ACCOUNT=partner \
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=partner \
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