SSE API

Broadcast Viewer

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 or 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_changedAccess type changed-
pass_code_changedPassword changed-
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

{
  "position": 3,
  "estimated_wait": "About 2 minutes"
}
FieldTypeDescription
positionnumberPosition in the queue (1 = next)
estimated_waitstringEstimated wait time

3. admitted - Entered the Live Stream

{
  "message": "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); not sent during the standby phase, counted from 00:00 in the live phase

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

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": "Live stream is paused"
}
FieldTypeDescription
messagestringNotification message

8. resumed - Resumed

{
  "message": "Live stream has resumed"
}
FieldTypeDescription
messagestringNotification message

9. ended - Ended

{
  "message": "Broadcast has ended"
}
FieldTypeDescription
messagestringEnd message

10. kicked - Kicked Out

{
  "message": "Removed by the host"
}

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

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

Sent when the host changes the broadcast's access type (public ↔ password). After receiving this event, the viewer's SSE connection is disconnected and must be reestablished.

This event carries no data (data is empty).


17. pass_code_changed - Password Changed

Sent when the host changes the broadcast password. After receiving this event, the viewer's SSE connection is disconnected and the password must be re-verified before reconnecting.

This event carries no data (data is empty).


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": "Broadcast has started"
}
FieldTypeDescription
phasestringNew phase: live (live phase)
messagestringPhase change message

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",
  "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": "azure_openai",
    "last_error_code": "llm_provider_error",
    "fail_count": 5
  }
}
FieldTypeDescription
error_codestringError code
messagestringError message
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)

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 foundVerify that the Token is correct
broadcast_session_not_started404Broadcast has not started yetRetry later
broadcast_session_ended410Broadcast endedNotify the user that the broadcast has ended
broadcast_token_revoked410Broadcast revokedNotify the user that the broadcast is unavailable
broadcast_capacity_exceeded503Viewer count has reached the limitJoin the waiting queue
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) => {
    console.log('You have been removed');
    eventSource.close();
  });

  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('access_type_changed', () => {
    console.log('Access type changed, reconnection required');
    eventSource.close();
    // Reconnect or guide the user to re-verify
  });

  eventSource.addEventListener('pass_code_changed', () => {
    console.log('Password changed, re-verification required');
    eventSource.close();
    // Guide the user to re-enter the password
  });

  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.5.7 Last Updated: 2026-05-20

Copyright © 2026