Skip to main content
Alongside the translated audio, the translator publishes JSON frames into the room over LiveKit text streams. There is no second connection to open and no separate endpoint to poll: if you are connected to the room, you are already receiving them. This page is the wire reference for those frames — what each one means, its exact shape, and the rules for reading it. For creating a room and connecting to it, see Build real-time AI translation in under 10 minutes.

Two topics

Frames are separated by topic, and the split is not cosmetic — the two carry different kinds of truth and are read with different rules.

glot.transcript

What is being said. Live chunks of each translation, then the confirmed utterance in both languages. This is what you render as captions.

glot.system

What the translator is doing. Dialling its model, retrying it, losing it. This is what keeps a stalled translation from looking like silence.
Both are plain JSON, broadcast to everyone in the room, and both carry a v field so you can recognize a frame you cannot read.
These are deliberately not LiveKit’s own lk.transcription topic. That convention assumes the sender is the participant being transcribed, and here the sender is a translator relaying somebody else’s speech — so LiveKit’s built-in transcription events never see these frames. Register handlers for glot.transcript and glot.system yourself.

Transcript frames

The model streams a translation before it confirms it, so an utterance arrives in two forms. Both carry a segment id, which is what ties them together.
One chunk of the translation as it is produced, for liveness only.
JSON
string
The chunk verbatim — a delta, not a running total. Accumulate deltas per segment for the typing effect.
boolean
Marks the partial that ended the utterance. It means “this stopped growing”, not “this is complete” — it exists so you can stop a cursor even when the segment frame never arrives.
Both kinds share:
integer
required
Frame-shape version, currently 1.
string
required
Groups the partials of one utterance with the segment frame that supersedes them.
string
required
ISO 639-1 code of the language this text was translated into.

System messages

The translator runs on a model it reaches over the network, and when that model is slow to answer or drops, the translated audio simply stops — which from your user’s side is indistinguishable from nobody talking. These messages are how you tell those two apart.
JSON
model.reconnecting is the only kind carrying extra fields:
integer
How many times in a row the connection has failed. It resets once a connection survives long enough to be considered healthy, so it says “how bad is this right now”, not “how many times has this call reconnected”.
number
Seconds until the next attempt. Relative, never a timestamp — your clock and the translator’s do not agree — so count down from the delta.
Every message carries:
integer
required
Envelope version, currently 1.
string
required
Dot-namespaced as subject.happening. The list above will grow.
string
required
info, warning, or error — so a kind you have never heard of is still rankable.
A reconnect starts a fresh session on the model, which has no memory of the utterance that was in flight when the connection died. Whatever was mid-sentence is never translated and no late caption is coming for it — that is a caption left permanently at stopped. Treat model.recovered as a resumption, not a catch-up.
Short interruptions are deliberately not announced. A connection that was healthy and comes straight back costs the room about a second, and firing model.reconnecting for that would flash a warning nobody needed. Expect messages for outages worth reporting, not for every blip.

Versions and unknown kinds

The translator and your client deploy independently, so both topics carry v — but what v covers differs, and so does what you do with a kind you don’t recognize. That asymmetry is the one thing to carry away from this page. A system parser that only admits the kinds it knows today will silently swallow every message added after your last release; a transcript parser that admits an unknown kind has nothing sensible to do with it.

Reading frames off a topic

Register handlers before you connect, so nothing published in the first moments of the session arrives without one. The quick start’s Reading frames off the topic shows the transcript handler in full — the system topic is registered on the same Room, the same way. Reads are chained rather than run concurrently. A text-stream handler returns void, so the SDK does not await it — two overlapping readAll() promises can settle out of order and interleave one utterance’s chunks into another’s. What is specific to having two topics is that each needs its own chain. Sharing the transcript’s would queue “the model is reconnecting” behind whatever captions are already in flight, and that is precisely the moment the message matters.
TypeScript
A Room throws if you register twice for the same topic — but glot.transcript and glot.system are different topics, so both handlers sit happily on one Room. Register each once, before room.connect().

Parsing

Parsing runs on network input inside a stream handler, where a throw takes out your read chain rather than surfacing anywhere useful. Both parsers below are total: they return null for anything they cannot read, and never raise.
Treat malformed JSON, a non-object, an unknown kind, and a future v all as “not a frame I can read”.
TypeScript

Folding into state

The two topics fold differently, and the difference follows from what they describe. A transcript is a growing list of utterances; the system state is one connection, so its messages collapse onto each other.

Transcript: extend, then replace

A partial extends its utterance, a segment replaces it. Appending a segment’s text would duplicate the whole utterance, because the segment carries what the partials already spelled out.
TypeScript
The three statuses are genuinely different states, not one flag: An utterance interrupted by a model reconnect stops without ever being confirmed, so it stays stopped forever. A UI that treated stopped as done would claim text was confirmed when it never was.

System: collapse onto the newest

These messages describe where one connection is, so a second model.reconnecting replaces the first. They arrive in order, so the newest wins outright.
TypeScript

Messages tell you what happened, attributes tell you what is true

This is the part to get right, and it is why a glot.system handler alone is not enough. A text stream is not replayed to a participant who joins after it was published, and the two terminal messages — model.unavailable and model.lost — are followed immediately by the translator leaving the room, so they can lose the race and never arrive at all. The translator therefore also keeps its current state on its participant attribute status, which is replayed to late joiners and is set before it leaves: So: drive your UI from the messages, and use status === "error" as the authority that overrides them. Without that fallback a client can sit on “reconnecting…” forever for a translator that has already given up and left.
TypeScript
Call it once more after room.connect() resolves, for the case where you joined a session already in progress.

Showing the translator’s status

Captions have their own guidance in Rendering captions. For the system state:
  • Say nothing when model is ready or null. A working translator needs no commentary, and a badge that is always present stops being read.
  • Pair it with, not instead of, your connection status. They fail independently: your WebRTC connection can be perfectly healthy while the translator has lost its model, and “Connected” alone would then claim translation is working.
  • Render the countdown only while there is one. retryInSeconds can be absent or effectively zero, and “retrying in 0s” reads as broken. Round up — a countdown showing 1s for 1.4 seconds finishes before the retry does.
  • Announce it with aria-live="polite". This narrates something the user cannot see happening — audio that has gone quiet — so a screen reader gets no signal otherwise.

Pitfalls

You are appending segment text to the accumulated partials. A segment replaces, it never extends.
Your reads run concurrently. Chain them per topic, as shown above.
The model reconnected. That starts a new session, which never receives the rest of the utterance — so its caption stays at stopped and no segment frame is coming. That is what stopped is for; don’t render it as confirmed. A model.reconnecting message is how you know this is what happened.
You are driving it from glot.system alone. model.lost is published as the translator shuts down and can lose that race, so it is not guaranteed to arrive. Read the translator’s status attribute as well and treat error as authoritative — see messages and attributes.
Expected. A connection that drops and comes straight back is not announced, because flashing a warning for a one-second gap is worse than staying quiet. Messages cover outages worth reporting, not every interruption.
Your parser is rejecting unknown kinds. New system kinds ship without a v bump, by design — keep any frame whose envelope is valid and ignore the ones your UI has no use for. See versions and unknown kinds.
Clear both the transcript and the system state when the token changes. A fresh token is a fresh session, and state that outlives the connection won’t reset itself.

See it working

Translation Playground, in the Glot dashboard, renders both topics side by side — watch partial and segment frames land in the live transcript, and the translator’s status change beside them.