SSE API

Broadcast Viewer SSE

Connection Information

ItemValue
Base pathhttps://vas-poc.vurbo.ai/broadcast
ProtocolHTTP + Server-Sent Events (SSE)
Data formattext/event-stream
AuthenticationToken authentication (no API Key required)

Note: The base path for Broadcast SSE differs from the other SSE APIs.


Endpoint Overview

MethodEndpointDescription
GET/broadcast/{token}/textViewer real-time subtitle stream

GET /broadcast/{token}/text

Description

Viewers connect through a share Token to receive an SSE stream of real-time transcription and translation.

Use Cases

  • Viewers watch real-time subtitles
  • Multi-language translation subtitle display
  • TTS audio playback

Authentication

Token authentication (no API Key required): verification is performed via the {token} in the URL path.

Password protection note: When a broadcast is configured with password protection, viewers must first obtain a viewer_access_token through the password verification API, then include this Token in the SSE connection's query parameter. See Authentication for details.

Request Parameters

ParameterLocationTypeRequiredDescription
tokenpathstringYesBroadcast share Token (4-character short code, character set a-z0-9)
langquerystringNoFilter to a specific translation language (e.g., en-US)
ttsquerybooleanNoWhether to enable TTS (true / false, defaults to false)
viewer_access_tokenquerystringConditionalViewer access Token (required for password-protected broadcasts)

Request Examples

// Receive all languages
const eventSource = new EventSource(
  'https://vas-poc.vurbo.ai/broadcast/a3f9/text'
);

// Receive only English translations
const eventSource = new EventSource(
  'https://vas-poc.vurbo.ai/broadcast/a3f9/text?lang=en-US'
);

// Receive English translations and enable TTS
const eventSource = new EventSource(
  'https://vas-poc.vurbo.ai/broadcast/a3f9/text?lang=en-US&tts=true'
);

// Password-protected broadcast
const eventSource = new EventSource(
  'https://vas-poc.vurbo.ai/broadcast/a3f9/text?viewer_access_token=xxx'
);

Event Types

EventDescriptionNotes
connectedConnection confirmation-
queuedAdded to the waiting queueQueueing mechanism
admittedEntered the live stream from the queueQueueing mechanism
originOriginal text (STT)-
translationTranslation result-
tts_readyTTS audio ready-
pausedBroadcast pausedHost paused manually (a host disconnection is reported as host_disconnected)
resumedBroadcast resumedHost resumed
endedBroadcast ended-
kickedKicked outViewer management
speaker_renamedSpeaker renamed-
speaker_reassignedSingle-sentence speaker reassigned-
speakers_mergedSpeakers merged-
recording_startedNew recording started-
max_viewers_changedViewer limit changed-
access_type_changedNote: No longer emitted — when access type changes to password-protected, viewers receive kicked instead (they must re-authenticate)-
pass_code_changedNote: No longer emitted — when the passcode changes, viewers receive kicked instead (they must re-authenticate)-
host_disconnectedThe host is temporarily offline and reconnecting (the broadcast is frozen, not ended)-
host_reconnectedThe host has reconnected (data.resumed indicates whether playback also resumed)-
standbyStandby phase notification-
phase_changedPhase change notification-
announcementHost announcement-
errorError-

Event Formats


1. connected - Connection Confirmation

{
  "available_langs": ["en-US", "ja-JP"],
  "tts_languages": ["en-US"],
  "phase": "standby",
  "recognition_mode": "single"
}
FieldTypeDescription
available_langsarrayList of available translation languages
tts_languagesarrayList of languages with TTS enabled (an empty array means no TTS)
phasestringBroadcast phase: standby or live
recognition_modestringRecognition mode: single (single speaker) or multi_speaker (multi-speaker diarization)

2. queued - In Queue

Note: several events below carry a message / estimated_wait string that the server returns as a fixed Traditional Chinese value. These are display strings only - render them as-is or substitute your own copy, and never parse or compare against them.

{
  "position": 3,
  "estimated_wait": "約 2 分鐘"
}
FieldTypeDescription
positionnumberPosition in the queue (1 = next)
estimated_waitstringEstimated wait time. Server-fixed value in Traditional Chinese: 少於 1 分鐘 ("less than 1 minute") or 約 N 分鐘 ("about N minutes")

3. admitted - Entered the Live Stream

{
  "message": "已進入直播"
}
FieldTypeDescription
messagestringNotification message. Server-fixed value in Traditional Chinese ("entered the live stream")

4. origin - Original Text (STT)

{
  "sid": 1,
  "text": "Hello everyone",
  "is_final": true,
  "language": "zh-TW",
  "speaker_id": "Guest-1",
  "speaker_label": "Royx",
  "start_time": "00:05"
}
FieldTypeDescription
sidnumberSentence ID
textstringOriginal text content
is_finalbooleanWhether this is the final result
languagestringOriginal text language
speaker_idstringOriginal speaker ID (conversation mode, optional; immutable)
speaker_labelstringDisplay label (conversation mode, optional; after applying speaker_aliases, equals speaker_id when no alias exists)
start_timestringSentence start time, format mm:ss (aligned with the host WS origin and History init_sentence), counted from 00:00 once the broadcast goes live (live)

Viewers receive only the original text and translations from after the broadcast goes live. Standby (standby) content is never sent to viewers, and viewers who connect after going live do not see it in earlier content either.


5. translation - Translation Result

{
  "sid": 1,
  "language": "en-US",
  "text": "Hello everyone",
  "speaker_id": "Guest-1",
  "speaker_label": "Royx",
  "is_final": true
}
FieldTypeDescription
sidnumberCorresponding sentence ID
languagestringTranslation language
textstringTranslated content
speaker_idstringOriginal speaker ID (conversation mode, optional; immutable)
speaker_labelstringDisplay label (conversation mode, optional; after applying speaker_aliases, equals speaker_id when no alias exists)
is_finalbooleanWhether this is the final result

The translation of a sentence may first arrive several times as interim results with is_final: false, followed by the finalized version with is_final: true. Match them by sid and language and replace what is displayed; to show only finalized results, skip events with is_final: false. A viewer who joins partway through receives the most recent few sentences of original text and finalized translations; interim results are not backfilled, and the backfill does not necessarily cover everything since the broadcast went live.


6. tts_ready - TTS Audio

{
  "sid": 1,
  "language": "en-US",
  "transcript": "你好",
  "text": "Hello",
  "audio": "base64...",
  "format": "mp3",
  "duration_ms": 2340,
  "boundaries": [
    {
      "offset_ms": 0,
      "duration_ms": 320,
      "text": "Hello",
      "text_offset": 0,
      "word_length": 5
    }
  ]
}
FieldTypeDescription
sidnumberCorresponding sentence ID
languagestringTTS language
transcriptstringOriginal transcript (original text)
textstringTranslated text
audiostringBase64-encoded MP3 audio
formatstringAudio format, always "mp3"
duration_msnumberAudio duration (milliseconds)
boundariesarrayWord boundaries (optional, see the table below)

Word Boundaries fields (each object in the boundaries array):

FieldTypeDescription
offset_msnumberStart time of the word in the audio (ms)
duration_msnumberPronunciation duration of the word (ms)
textstringWord text
text_offsetnumberStarting position of the word in the text
word_lengthnumberCharacter length of the word

Note:

  • The host must specify which languages have TTS enabled via the tts_config parameter in the start command
  • Only viewers who subscribe to that language and have TTS enabled will receive this event
  • It is sent only during the live phase; no TTS is sent during the standby phase

7. paused - Paused

{
  "message": "廣播已暫停"
}
FieldTypeDescription
messagestringNotification message. Server-fixed value in Traditional Chinese ("the broadcast is paused")

8. resumed - Resumed

{
  "message": "廣播已恢復"
}
FieldTypeDescription
messagestringNotification message. Server-fixed value in Traditional Chinese ("the broadcast has resumed")

9. ended - Ended

{
  "message": "廣播已結束"
}
FieldTypeDescription
messagestringEnd message. Server-fixed value in Traditional Chinese ("the broadcast has ended")

10. kicked - Kicked Out

{
  "message": "Kicked by host"
}
FieldTypeDescription
messagestringReason for removal; display only, never parse it. When the host removes a viewer the value is the fixed string Kicked by host; when the removal is caused by an access-type or passcode change, the value is a Traditional Chinese explanation of that change

11. speaker_renamed - Speaker Renamed

For conversation mode only. Sent when the host performs a global rename of a speaker.

{
  "speaker_id": "Guest-1",
  "new_label": "Royx",
  "affected_sids": [1, 3, 5]
}
FieldTypeDescription
speaker_idstringResolved original speaker ID (even if the input is a display label, the event returns the original ID)
new_labelstringNew display label (e.g., Royx)
affected_sidsarrayList of affected sentence IDs

12. speaker_reassigned - Speaker Identity Reassigned

For conversation mode only. Sent when the host reassigns the speaker of a single sentence.

{
  "sid": 3,
  "old_speaker_id": "Guest-1",
  "new_speaker_id": "Guest-2",
  "new_speaker_label": "Amy"
}
FieldTypeDescription
sidnumberThe reassigned sentence ID
old_speaker_idstringOriginal speaker ID (e.g., Guest-1)
new_speaker_idstringNew original speaker ID (e.g., Guest-2)
new_speaker_labelstringNew speaker display label (after applying speaker_aliases; equals new_speaker_id when no alias exists)

13. speakers_merged - Speakers Merged

For conversation mode only. Sent when the host merges speakers. After the merge, all sentences of that speaker are reassigned to the target speaker.

{
  "source_speaker_id": "Guest-2",
  "target_speaker_id": "Guest-1",
  "target_speaker_label": "Manager Wang",
  "affected_sids": [3, 5, 7]
}
FieldTypeDescription
source_speaker_idstringOriginal ID of the merged speaker (e.g., Guest-2)
target_speaker_idstringOriginal ID of the merge target speaker (e.g., Guest-1)
target_speaker_labelstringTarget speaker display label (after applying speaker_aliases; equals the original ID when no alias exists)
affected_sidsarrayList of affected sentence IDs: sentences that belonged to the source speaker, plus the target speaker's existing sentences whose display name changed because of the merge (for example, when the source speaker's custom name is carried over to the target)

14. recording_started - New Recording Started

Sent when the host starts a new recording segment. After receiving this event, viewers should clear the old subtitles on screen.

This event carries no data (data is empty).


15. max_viewers_changed - Viewer Limit Changed

Sent when the host changes the viewer limit.

{
  "max_viewers": 200
}
FieldTypeDescription
max_viewersnumberNew viewer limit

16. access_type_changed - Access Type Changed

Note: This event is retired (the name is kept for compatibility) and the server never emits it. When the access type changes to password-protected, viewers receive kicked instead (with message explaining the change) and must re-verify the password before reconnecting.


17. pass_code_changed - Password Changed

Note: This event is retired (the name is kept for compatibility) and the server never emits it. When the passcode changes, viewers receive kicked instead (with message explaining the change) and must re-verify the password before reconnecting.


18. standby - Standby Phase

When a viewer connects during the standby phase, they receive this event immediately after the connected event, indicating that the broadcast has not officially started yet. The host can dynamically update the standby message via the WebSocket set_standby_message action; after the update, all viewers receive a new standby event.

{
  "message": "The presentation is about to begin...",
  "translations": {
    "en-US": "The presentation is about to begin...",
    "ja-JP": "プレゼンテーションがまもなく始まります..."
  }
}
FieldTypeDescription
messagestringStandby phase display message (original text)
translationsobjectTranslation results (optional); the key is the language code and the value is the translated text

19. phase_changed - Phase Change

Sent when the broadcast switches from the standby phase to the live phase.

{
  "phase": "live",
  "message": "廣播已開始"
}
FieldTypeDescription
phasestringNew phase: live (live phase)
messagestringPhase change message. Server-fixed value in Traditional Chinese ("the broadcast has started")

20. announcement - Announcement

An announcement message sent by the host; all viewers receive it.

{
  "message": "The meeting will end in 5 minutes",
  "translations": {
    "en-US": "The meeting will end in 5 minutes",
    "ja-JP": "会議は5分後に終了します"
  }
}
FieldTypeDescription
messagestringAnnouncement message content (original text)
translationsobjectTranslation results (optional); the key is the language code and the value is the translated text

21. error - Error

{
  "error_code": "broadcast_session_ended",
  "message": "Broadcast session ended",
  "severity": "error"
}

Sentence-level errors (such as a translation failure for a specific language) additionally carry sid and translation_language, allowing the viewer to mark a specific language failure for a specific sentence:

{
  "error_code": "llm_content_filtered",
  "severity": "warning",
  "message": "Content filtered",
  "context": "translation",
  "sid": 5,
  "translation_language": "ja-JP",
  "timestamp": "2026-04-26T10:30:45.123Z"
}

Session-level translation service errors (escalated when consecutive failures reach a threshold, added in v1.3.8) do not carry sid or translation_language, are broadcast to all viewers, and the frontend should display a global notice while keeping the connection alive:

{
  "error_code": "translation_service_unavailable",
  "severity": "error",
  "message": "Translation service unavailable",
  "context": "translation",
  "timestamp": "2026-04-26T10:30:45.123Z",
  "details": {
    "provider": "llm_service",
    "last_error_code": "llm_provider_error",
    "fail_count": 5
  }
}
FieldTypeDescription
error_codestringError code
messagestringError message (a fixed English display string returned by the server; display only, never parse it)
severitystringSeverity: warning / error / fatal
contextstringOptional. Error context (such as broadcast, translation)
sidintOptional. Sentence number for a sentence-level error (e.g., a translation failure for that sentence); not carried for session-level errors
translation_languagestringOptional. Target language of the translation failure (viewers use this to determine whether it is related to that language)
detailsobjectOptional. Debug information (e.g., translation_service_unavailable carries provider, last_error_code, fail_count)
timestampstringOptional. Time the error occurred (ISO 8601)
request_idstringOptional. Request tracking ID; present only on connection-phase errors and queue-timeout errors, omitted on sentence-level / session-level errors

22. host_disconnected - Host Disconnected

Sent when the host's connection drops and the broadcast enters the reconnect grace period. The broadcast is frozen but not ended, so viewers should not close the connection - show a notice and wait for host_reconnected or ended.

{
  "message": "主辦暫時離線,重連中"
}
FieldTypeDescription
messagestringNotification message. Server-fixed value in Traditional Chinese ("the host is temporarily offline and reconnecting")

23. host_reconnected - Host Reconnected

Sent when the host successfully resumes the session.

{
  "message": "主辦已重新連線",
  "resumed": true
}
FieldTypeDescription
messagestringNotification message. Server-fixed value in Traditional Chinese ("the host has reconnected")
resumedbooleanWhether playback resumed as well. false means the host had paused manually before disconnecting, so the broadcast stays paused

Heartbeat Mechanism

The SSE connection uses a heartbeat to keep the connection alive:

  • Interval: 15 seconds
  • Format: SSE comment (starting with :)
  • No frontend handling is needed; the browser ignores it automatically
: heartbeat

Specific Error Codes

Error CodeHTTP StatusDescriptionRecommended Handling
broadcast_session_not_found404Broadcast not found (returned for both unknown and revoked tokens)Verify that the Token is correct
broadcast_not_ready503Token is valid, but the broadcast has not started yet or has already endedRetry later
broadcast_session_not_started503Broadcast has not started yetRetry later
broadcast_session_ended410Broadcast endedNotify the user that the broadcast has ended
broadcast_capacity_exceeded503Both the viewer slots and the waiting queue are fullRetry later
broadcast_queue_timeout- (error event on an open connection)Waited in the queue for more than 5 minutesReconnect
broadcast_password_required401Password verification requiredGuide the viewer to enter the password and obtain a viewer_access_token
translation_service_unavailable-Translation service consecutive failures reached the threshold (added in v1.3.8, no sid)Display a global notice "Translation temporarily unavailable", no disconnection needed; the original text (STT) continues to stream normally

Frontend Example

function connectBroadcast(token, lang = null) {
  let url = `https://vas-poc.vurbo.ai/broadcast/${token}/text`;
  if (lang) {
    url += `?lang=${lang}`;
  }

  const eventSource = new EventSource(url);

  eventSource.addEventListener('connected', (e) => {
    const data = JSON.parse(e.data);
    console.log(`Phase: ${data.phase}, Mode: ${data.recognition_mode}`);
    console.log(`Available translations: ${data.available_langs.join(', ')}`);
  });

  eventSource.addEventListener('queued', (e) => {
    const data = JSON.parse(e.data);
    console.log(`In queue, position: ${data.position}, estimated wait: ${data.estimated_wait}`);
  });

  eventSource.addEventListener('admitted', (e) => {
    console.log('Entered the live stream');
  });

  eventSource.addEventListener('origin', (e) => {
    const data = JSON.parse(e.data);
    console.log(`[SID ${data.sid}] ${data.text}`);
  });

  eventSource.addEventListener('translation', (e) => {
    const data = JSON.parse(e.data);
    console.log(`Translation (${data.language}): ${data.text}`);
  });

  eventSource.addEventListener('tts_ready', (e) => {
    const data = JSON.parse(e.data);
    // Decode the Base64 MP3 and play it
    const audioBlob = base64ToBlob(data.audio, 'audio/mp3');
    const audioUrl = URL.createObjectURL(audioBlob);
    const audio = new Audio(audioUrl);
    audio.play();
  });

  eventSource.addEventListener('paused', (e) => {
    const data = JSON.parse(e.data);
    console.log(`Live stream paused: ${data.message}`);
  });

  eventSource.addEventListener('resumed', (e) => {
    console.log('Live stream has resumed');
  });

  eventSource.addEventListener('ended', (e) => {
    const data = JSON.parse(e.data);
    console.log(`Live stream ended: ${data.message}`);
    eventSource.close();
  });

  eventSource.addEventListener('kicked', (e) => {
    const data = JSON.parse(e.data);
    console.log(`Left the live stream: ${data.message}`);
    eventSource.close();
    // Access-type and passcode changes also land here; guide the user to re-verify before reconnecting
  });

  eventSource.addEventListener('speaker_renamed', (e) => {
    const data = JSON.parse(e.data);
    console.log(`Speaker renamed: ${data.speaker_id} → ${data.new_label}`);
    // Update the display labels of all affected sentences
  });

  eventSource.addEventListener('speaker_reassigned', (e) => {
    const data = JSON.parse(e.data);
    console.log(`Speaker of sentence ${data.sid} changed to: ${data.new_speaker_label}`);
    // Update the display label of that sentence
  });

  eventSource.addEventListener('speakers_merged', (e) => {
    const data = JSON.parse(e.data);
    console.log(`Speakers merged: ${data.source_speaker_id} → ${data.target_speaker_label}`);
    // Update the display labels of all affected sentences
  });

  eventSource.addEventListener('recording_started', () => {
    console.log('New recording started, clearing old subtitles');
    // Clear the old subtitles on screen
  });

  eventSource.addEventListener('max_viewers_changed', (e) => {
    const data = JSON.parse(e.data);
    console.log(`Viewer limit changed to: ${data.max_viewers}`);
  });

  eventSource.addEventListener('host_disconnected', (e) => {
    const data = JSON.parse(e.data);
    console.log(`Host offline: ${data.message}`);
    // Show a "host is reconnecting" notice; do not close the connection
  });

  eventSource.addEventListener('host_reconnected', (e) => {
    const data = JSON.parse(e.data);
    console.log(`Host is back: ${data.message} (playback resumed: ${data.resumed})`);
  });

  eventSource.addEventListener('standby', (e) => {
    const data = JSON.parse(e.data);
    const displayLang = 'en-US'; // The language selected by the viewer
    const displayMessage = data.translations?.[displayLang] || data.message;
    console.log(`Standby phase: ${displayMessage}`);
  });

  eventSource.addEventListener('phase_changed', (e) => {
    const data = JSON.parse(e.data);
    console.log(`Phase change: ${data.phase} - ${data.message}`);
    // Remove the waiting screen and start displaying subtitles
  });

  eventSource.addEventListener('announcement', (e) => {
    const data = JSON.parse(e.data);
    const displayLang = 'en-US';
    const displayMessage = data.translations?.[displayLang] || data.message;
    console.log(`Announcement: ${displayMessage}`);
  });

  eventSource.addEventListener('error', (e) => {
    if (e.data) {
      const error = JSON.parse(e.data);
      console.error(`Error [${error.error_code}]: ${error.message}`);
    }
    eventSource.close();
  });

  return eventSource;
}

// Base64-to-Blob utility function
function base64ToBlob(base64, mimeType) {
  const byteCharacters = atob(base64);
  const byteNumbers = new Array(byteCharacters.length);
  for (let i = 0; i < byteCharacters.length; i++) {
    byteNumbers[i] = byteCharacters.charCodeAt(i);
  }
  const byteArray = new Uint8Array(byteNumbers);
  return new Blob([byteArray], { type: mimeType });
}

Version: V1.24.1 Last Updated: 2026-10-07

Copyright © 2026