DocsAPI Reference

Recording API

Live class recording — start, stop, and Jibri webhook

OptiLearn embeds Jitsi Meet for live classes (Sprint ε Track 2). Recording is wired through Jibri (Jitsi Broadcasting Infrastructure). The OptiLearn endpoints orchestrate the host-facing UI; provisioning Jibri itself is ops work — see Jibri Recording Setup.

Wrapper: src/lib/jitsi-recording.ts. Webhook handler verifies an HMAC signature.

POST /api/live-sessions/[id]/recording/start

Host-only. Tells Jibri to start recording the session's room.

Pre-conditions

CheckFailure
Session exists in caller's institution404
Caller is the host (canManageSession)403
Session status is LIVE (recording before /start would target a non-existent room)409
Session has a roomName409
Not already in RECORDING or PENDING (idempotent — returns existing state)200 with alreadyRecording: true

Flow

  1. Optimistically flip recordingStatus = PENDING — prevents two rapid clicks firing two Jibri starts
  2. Call startJibriRecording({ roomName, callbackUrl, sessionId })
  3. On success: flip recordingStatus = RECORDING, stamp recordingStartedAt, clear stale stop fields
  4. On failure: roll back to the prior status

Response

{
  "data": {
    "recordingStatus": "RECORDING",
    "recordingStartedAt": "2026-04-15T14:02:00Z"
  }
}

When Jibri isn't provisioned (env vars missing or meet server returns 404), responds 503 with the friendly message so the UI can show "contact your administrator" instead of a generic 500.

Callback URL

Built from NEXTAUTH_URL (preferred) or fallback https://learn.opticrm.app:

<baseUrl>/api/webhooks/jitsi-recording

Jibri must be able to POST back to this URL — for local dev that means a tunnel (ngrok / cloudflared / etc).

POST /api/live-sessions/[id]/recording/stop

Host-only counterpart to /start. Tells Jibri to stop, flips the session to PROCESSING and stamps recordingStoppedAt. The actual recordingUrl arrives later via the webhook.

Idempotent — stopping a non-RECORDING/PENDING session is a no-op (returns alreadyStopped: true).

Failure mode

If Jibri is unreachable on stop, the local state moves to FAILED (rather than leaving a stuck RECORDING red dot) so the host can recover. Returns 503 with the friendly message.

Response

{
  "data": {
    "recordingStatus": "PROCESSING",
    "recordingStoppedAt": "2026-04-15T15:07:00Z"
  }
}

POST /api/webhooks/jitsi-recording

Public webhook endpoint Jibri calls when a recording finishes uploading. Signature-verified — no session cookie.

Authentication

HMAC-SHA256 over the raw request body, signed with JITSI_RECORDING_WEBHOOK_SECRET. The signature header may be plain hex or sha256= prefixed (GitHub-style):

X-Jibri-Signature: <hex>          # or
X-Jibri-Signature: sha256=<hex>    # also accepted
X-Jitsi-Signature: <hex>           # alias

If JITSI_RECORDING_WEBHOOK_SECRET is unset on the server, the endpoint refuses everything with 503 — refusing rather than defaulting prevents anyone on the public internet from posting arbitrary recording URLs.

Payload

{
  "event": "recording_uploaded",      // or "recording_failed"
  "sessionId": "<LiveSession.id>",    // echoed back from /start
  "roomName": "optilearn-<slug>-<id>", // safety net
  "recordingUrl": "https://...",      // R2 URL, only on success
  "duration": 1834                    // seconds, optional
}

Handling

  • recording_failed → set recordingStatus = FAILED, recordingStoppedAt = now. Ack 200.
  • recording_uploaded → store recordingUrl, set recordingStatus = READY, store recordingDuration. Then fan LIVE_CLASS_RECORDING_READY out to every active/completed enrollment via notificationService.sendBulk. Fan-out is fire-and-forget — the webhook ack does not wait on email/FCM.
  • roomName mismatch → 400 (misrouted webhook; refuse rather than corrupt state)
  • Unknown session → 200 with ignored: true (don't 404; Jibri retries on non-2xx)
  • Bad signature → 401

Response

{ "data": { "ok": true, "status": "READY" } }

Recording status state machine

NULL ── start ──▶ PENDING ── start succeeds ──▶ RECORDING
  ▲                  │                              │
  │                  ├─ start fails ────────────────┘
  │                  │
  │                  ▼
  │              (rollback)
  │                                                 │
  │                                          stop ──┘
  │                                                 ▼
  │                                            PROCESSING
  │                                                 │
  │                              webhook recording_uploaded ──▶ READY
  │                              webhook recording_failed   ──▶ FAILED
  │                              stop while Jibri unreachable ─▶ FAILED

Required environment variables

Both must be set on the OptiLearn server for recording to work end-to-end:

VariablePurpose
JITSI_RECORDING_API_TOKENBearer token used by startJibriRecording / stopJibriRecording to call the Jibri-enabled meet server
JITSI_RECORDING_WEBHOOK_SECRETHMAC secret for verifying inbound Jibri callbacks

Without these, /start returns 503 and the webhook refuses 503. See Jibri Recording Setup for ops.