SSE API

Floating Subtitle SSE

Overview

The Floating Subtitle SSE lets you subscribe, over an independent, read-only connection, to the live transcript of an in-progress recording (source-language original text plus target-language translations). It is designed for desktop floating-subtitle windows, second-screen captions, and similar use cases. This feed is separate from the recording's own WebSocket connection, so it can be opened independently on a different device or window. "In-progress recordings" include live broadcasts — a broadcast host can also use this feed to subscribe to their own live transcript during a broadcast.

Note: The base path for Floating Subtitle SSE is https://vas-poc.vurbo.ai (the real-time service, same origin as the WebSocket), which differs from the general SSE API base https://vas-poc.vurbo.ai/api/v1/sse.


Connection Information

ItemValue
Base pathhttps://vas-poc.vurbo.ai
ProtocolHTTP + Server-Sent Events (SSE)
Data formattext/event-stream
Authenticationfeed_token (bound to the recording, no API Key required)

Endpoint Overview

MethodEndpointDescription
POST/api/v1/auth/tasks/{taskId}/subtitle-feed-tokenExchange for a floating-subtitle feed_token (see Subtitle Feed Token API)
GET/tasks/{task_id}/subtitleReal-time transcript SSE stream

Connection Flow

  1. The recording owner calls POST /api/v1/auth/tasks/{taskId}/subtitle-feed-token with an API Key to exchange for a short-lived feed_token.
  2. The floating-subtitle window connects to GET /tasks/{task_id}/subtitle with that feed_token to receive the SSE stream.
  3. The feed_token can be reused for reconnection while valid (each successful validation extends its validity); for long recordings, exchange for a new token before expiry.

On-site audience members can also connect via the owner's share link: exchange the share secret for an audience Token (see Subtitle Feed Token API), then connect to this SSE with that Token.


GET /tasks/{task_id}/subtitle

Description

Subscribe read-only to the live transcript of an in-progress recording, receiving an SSE stream of original text (both interim and finalized) and translation results. Supports cross-device and cross-window connections, with automatic reconnection and replay on reconnect.

Use Cases

  • Persistent desktop floating-subtitle window
  • Second-screen / projected caption display
  • Bilingual (original + translation) real-time subtitles

Authentication

feed_token authentication (no API Key required): verified via the feed_token query parameter. The token is bound to the recording and uses a sliding expiry (each successful validation extends it).

Request Parameters

ParameterLocationTypeRequiredDescription
task_idpathstringYesRecording ID
feed_tokenquerystringYesFloating-subtitle access Token (obtained from the token endpoint)
langquerystringNoFilter target translation languages; comma-separated (e.g., en-US,ja-JP). Omit or * for all

Request Examples

// Receive all languages
const eventSource = new EventSource(
  'https://vas-poc.vurbo.ai/tasks/3f9a.../subtitle?feed_token=xxx'
);

// Receive only English translation
const eventSource = new EventSource(
  'https://vas-poc.vurbo.ai/tasks/3f9a.../subtitle?feed_token=xxx&lang=en-US'
);

Connection Timing Boundaries

SituationHTTP StatusRecommended Handling
Recording has not started (or timed out and disappeared)425 Too EarlyRetry later
Recording has ended410 GoneInform the user and close the window
feed_token invalid / expired401 UnauthorizedObtain a new feed_token
Connection or audience limit reached429 Too Many RequestsRetry later or close extra windows

Event Types

EventDescriptionNotes
connectedConnection confirmationIncludes current status and language info
resultOriginal text (STT) or real-time translationCarries origin or translations depending on payload
translationSingle-sentence re-translation resultCarries is_retranslation
batch_retranslationBatch re-translation resultWhen switching languages
language_switch_start / language_switch_doneBatch re-translation progressWhen switching languages in interpretation mode
language_switchedInterpretation language switchInterpretation mode
translation_language_removedA translation language was removedAfter a successful multi-language switch_language with op=remove
segment_discardedSegment discardedThat sid will receive no further events; clear it from any "translating" state
statusStatus notificationPause / resume / stop
viewersCurrent viewer countWhen audience sharing is enabled
subtitle_closedSharing closed, connection endedAudience side; stop reconnecting on receipt
speaker_renamedSpeaker renamedMulti-speaker mode
speaker_reassignedSingle-sentence speaker changeMulti-speaker mode
speakers_merged / speakers_auto_mergedSpeakers mergedMulti-speaker mode

Each event's data is identical to the corresponding message on the recording host's WebSocket; see WebSocket Events for full field definitions. Only the commonly used floating-subtitle events are listed below.

Event Formats


1. connected - Connection Confirmation

{
  "status": "live",
  "recognition_mode": "single",
  "source_lang": "zh-TW",
  "translation_languages": ["en-US", "ja-JP"]
}
FieldTypeDescription
statusstringRecording status: live / paused / reconnecting / ended
recognition_modestringRecognition mode: single / multi_speaker / multi_language
source_langstringSource language (non-interpretation mode)
conversation_languagesarrayThe two languages in interpretation mode (present only in interpretation mode, replaces source_lang)
translation_languagesarrayList of target translation languages (the current authoritative set: reflects mid-recording additions/removals, not the value at recording start). Omitted entirely when there is no target language

2. result - Original Text (STT)

When data carries origin, it is original text:

{
  "action": "result",
  "origin": {
    "sid": 1,
    "language": "zh-TW",
    "detected_language": "zh-TW",
    "text": "大家好",
    "is_final": true,
    "speaker_id": "Guest-1",
    "speaker_label": "Royx",
    "start_time": "00:05"
  }
}
FieldTypeDescription
sidnumberSentence ID
languagestringSource language. In multi-language transcription it is determined per sentence and varies with the language actually spoken
detected_languagestringDetected language of this sentence (for multi-language/interpretation positioning; in multi-speaker mode it is the fixed source language and must not be used as a language badge)
textstringOriginal text
is_finalbooleanfalse = interim (will be replaced in place by later results with the same sid); true = finalized
speaker_idstringOriginal speaker ID (multi-speaker mode, optional)
speaker_labelstringDisplay label (multi-speaker mode, optional)
start_timestringSentence start time, format mm:ss

3. result - Real-time Translation

When data carries translations, it is a translation (real-time translation is one message per language):

{
  "action": "result",
  "translations": {
    "en-US": {
      "sid": 1,
      "text": "Hello everyone",
      "is_final": true
    }
  }
}
FieldTypeDescription
translationsobjectKey is the target language code, value is the translation result
translations.<lang>.sidnumberCorresponding original sentence ID
translations.<lang>.textstringTranslation text
translations.<lang>.is_finalbooleanWhether this is the final result

Speaker inheritance: Translation messages do not carry speaker_id / speaker_label. The client should match by sid to an already-received original line and reuse its speaker information.


4. translation / batch_retranslation - Re-translation

When the user triggers re-translation during recording, an updated translation is sent with the same sid; the client should replace in place:

{
  "action": "translation",
  "sid": 1,
  "translations": {
    "en-US": {
      "sid": 1,
      "text": "Hi everyone",
      "is_final": true,
      "is_retranslation": true
    }
  }
}

5. status - Status Notification

{
  "action": "status",
  "status": "paused",
  "message": "Speech recognition paused"
}
FieldTypeDescription
statusstringMachine-readable recording lifecycle state: live (resumed) / paused / ended (stopped). The client should act on this field: paused → freeze the display, ended → close the floating subtitle window, live → resume the display.
messagestringHuman-readable status text (format not guaranteed; do not parse it to determine state — always rely on the status field).

When to close the floating subtitle window: the floating subtitle SSE stream does not close automatically when the recording stops (the connection is kept open by design). The host's own floating subtitle window must close itself on this event's status: "ended", otherwise it will freeze on the last sentence. The status field appears only on the pause / resume / stop lifecycle transitions; other status events, such as set_name and start_speaking in manual conversation mode, do not carry it.


6. speaker_renamed / speaker_reassigned / speakers_merged - Speaker Events

Multi-speaker mode only; fields are identical to the corresponding events in WebSocket Events. On receipt, the client updates the display labels of the relevant sentences.

{
  "action": "speaker_renamed",
  "speaker_id": "Guest-1",
  "new_label": "Royx",
  "affected_sids": [1, 3, 5]
}

7. language_switched - Interpretation Language Switch

Interpretation mode only.

{
  "action": "language_switched",
  "active_lang": "en-US",
  "translation_lang": "zh-TW"
}

8. viewers - Viewer Count

When audience sharing is enabled, the feed pushes the current viewer count; sent once on connection, then updated whenever the count changes.

{
  "count": 3,
  "max": 10
}
FieldTypeDescription
countnumberCurrent number of audience members (excluding the owner)
maxnumberAudience limit (server-configured, default 10; rely on this field rather than hardcoding a value)

9. subtitle_closed - Sharing Closed

When the host "closes sharing" or "stops the recording", the server proactively sends this event and then ends the audience connection (the audience side is a passive receiver).

{ "action": "subtitle_closed" }

On receipt, the audience client should show "sharing has ended" and stop reconnecting (sharing is now closed, so reconnection is rejected). The host's own connection is unaffected by this event.


Replay and Reconnection

  • On connection, the most recent finalized original and translation lines are replayed first (for late joiners / reconnection).
  • Speaker events are included in replay: speaker_renamed / speaker_reassigned / speakers_merged / speakers_auto_merged are replayed in original order (after the sentences they affect). Clients should handle them during replay exactly as in live mode — retroactively update the speaker labels of existing sentences by affected_sids — so reconnecting or late-joining viewers do not see pre-rename speaker names.
  • EventSource reconnects automatically; as long as the feed_token is still valid, finalized content from the interruption is replayed again after reconnection. Interim (is_final:false) drafts are not replayed and are naturally updated by subsequent results.

Heartbeat

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

  • Interval: 15 seconds
  • Format: SSE comment (starting with :)
  • No client handling required; browsers ignore it automatically
: heartbeat

Frontend Example

async function connectSubtitle(taskId, apiKey, lang = null) {
  // 1. Exchange for a feed_token
  const res = await fetch(
    `https://vas-poc.vurbo.ai/api/v1/auth/tasks/${taskId}/subtitle-feed-token`,
    { method: 'POST', headers: { 'X-API-Key': apiKey } }
  );
  if (res.status === 425) {
    // Recording not ready yet; retry after a short delay
    return;
  }
  const { token } = await res.json();

  // 2. Connect to SSE
  let url = `https://vas-poc.vurbo.ai/tasks/${taskId}/subtitle?feed_token=${token}`;
  if (lang) url += `&lang=${lang}`;
  const eventSource = new EventSource(url);

  eventSource.addEventListener('connected', (e) => {
    const data = JSON.parse(e.data);
    console.log(`Status: ${data.status}, source: ${data.source_lang}`);
  });

  eventSource.addEventListener('result', (e) => {
    const data = JSON.parse(e.data);
    if (data.origin) {
      // Original: replace in place by sid + is_final
      console.log(`[${data.origin.sid}] ${data.origin.text}`);
    } else if (data.translations) {
      for (const [lang, t] of Object.entries(data.translations)) {
        console.log(`Translation (${lang}): ${t.text}`);
      }
    }
  });

  eventSource.addEventListener('status', (e) => {
    console.log(`Status: ${JSON.parse(e.data).message}`);
  });

  eventSource.onerror = () => {
    // 410 = ended, 401 = token invalid; EventSource reconnects automatically, re-exchange the token if needed
  };

  return eventSource;
}

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

Copyright © 2026