Skip to main content
Glot translates a live conversation as it happens. Your client streams microphone audio into a room, a translator joins that room, and it publishes two things back: translated audio you play, and transcript frames you render as captions. This page describes the streaming model end to end — the lifecycle of a session, how audio flows, and the exact wire format of the transcript. It is the concept guide behind the Translation Playground, which is a working reference client for everything below.
Audio is carried over WebRTC by LiveKit, so you connect with a LiveKit client SDK using the url and token the Glot API returns. You never talk to LiveKit’s API directly — Glot provisions the room and mints the tokens.

How a session works

The moving parts

A live translation session, and also a billing record. Created with POST /v1/rooms. A room has no languages of its own — you can create it before you know who will join or what they speak. The response carries the room id (used to look up usage and to close the room) and the room name (used to mint tokens).
One connection to a room. Every participant, including the first, enters with a join token from POST /v1/rooms/{room_name}/token, which is where languages are chosen — one participant at a time. Tokens are short-lived and bound to the identity they were issued for; a participant is disconnected when their token expires.
ISO 639-1 codes, from GET /v1/supported-languages. A participant declares the languages they speak and want to hear. Send two when a single connection covers both sides of the conversation — two colleagues sharing one phone, or a test client like the playground — and one when the speaker has a device to themselves. That count is also what sets the room’s streaming mode. A room translates between at most two languages; a token request that would introduce a third returns 409.
Joins the room on its own once the room holds two languages, and leaves when fewer than two remain. You never dispatch or dismiss it. It publishes an audio track and transcript frames. It bills at a higher per-minute rate than a participant, which is why it is not present in an idle room.

Streaming modes: mono and dual

Every room has a streaming mode, which says how its audio reaches the translator. It follows from one question: are both speakers on the same device, or does each have their own? The mode decides how many lanes the room’s audio travels on — a lane being one uplink stream of mixed audio going to the translator.

mono — one shared device

Two people, one phone or laptop. Their voices are already mixed by the device’s mic, so the room has one lane carrying both languages.

dual — one device each

Each speaker joins from their own device, wherever they are. The room has one lane per language, so the two sides are never mixed together.
Both modes translate in both directions, publish audio the same way, and emit the same transcript frames. What differs is how the audio is carried — which is why the mode has to be consistent across a room’s tokens. A mono lane is expected to contain both voices; a dual lane is expected to contain one language’s speakers and nobody else.
A dual room is not limited to two participants — only to two distinct languages. Three people, two speaking English and one speaking Chinese, is a valid dual room: the two English devices are mixed together into the English lane.

Set the mode, or let Glot infer it

Pass streaming_mode when you create the room. Use this when you already know the shape of the call.
cURL
The room comes back with the mode it recorded, and from then on a join token asking for the wrong number of languages is an error rather than a room that quietly means something else.
JSON
Either way, the mode is fixed for the room’s lifetime — there is no call that changes it — and GET /v1/rooms/{room_id} reports whatever the room settled on.

Every later token must match

Once a room has a mode, a join token whose language count disagrees is refused with 409. The message names the mode and the count it expects:
409 Conflict
So a dual room takes exactly one language per token, and a mono room takes two on every connection.
An inferred mode is fixed by the first token even if nobody ever connects with it. Mint a one-language token by mistake against a room with no mode, and that room is dual for good — your only remedy is a new room. Declaring streaming_mode at creation turns that mistake into a 409 on the token instead.

Start a session

1

Create the room

Languages are not part of this call. Pass room_name to choose your own name — it must be unique among rooms that are currently live — or omit it and one is generated. Pass streaming_mode to pin the room to mono or dual, or omit it and let the first join token decide.
cURL
JSON
The room needs credit: a request against an organization whose balance is at or below zero returns 402. A call already in progress is never cut off, so a long room can run the balance negative.
2

Mint a join token

Use the room name from the previous response, not the name you asked for — when you omit room_name the API generates one.One token per connection. The example below carries both languages, which is the mono shape — a shared device. For a dual room, mint one token per speaker with a single language each. See streaming modes.
cURL
JSON
A join token is a live credential for a translation session. Mint it on your server and hand only the token to the browser — never ship your API key to a client.
3

Connect and publish the microphone

Nothing is translated until audio arrives, and the translator only joins once the room holds two languages.
TypeScript
4

Close the room when the call ends

Addressed by the room’s id — the persisted UUID — not the name tokens are minted against.
cURL
Metering is per participant, and we make a best effort to stop it as each one disconnects — so leaving the room does not silently run up minutes. But the room itself stays open until you close it or it ends on its own, so close it explicitly rather than relying on either. Closing an already-ended room is a no-op that still returns 204, so a retry on the way out is safe. The usage record survives — a room is a billing record and outlives the call it measured.

Play the translated audio

The translator publishes one audio track per language in the room, as ordinary remote tracks. In a mono room the shared device wants all of them — both speakers are listening to the same speakers, in both directions. So attach every remote audio track you get subscribed to, and detach it when it goes away.
TypeScript
In a dual room, each client should play only the translation into its own language — the language its token was minted with. Attaching every track there means a speaker also hears their own words coming back translated, which is what the other device is meant to play. Each translated track is named after the language it carries, so a client can pick the one it wants rather than attaching all of them.
The elements can live in a hidden container — they are playback sinks, not UI. Browsers block autoplay until the user has interacted with the page, so connect from a click rather than on page load.

Live transcription

The translator publishes one JSON frame per text stream on the topic glot.transcript, over the same room connection. There is no separate transcription endpoint or socket to open.
This is 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 a handler for glot.transcript yourself.

Two kinds of frame

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.
integer
required
Frame-shape version, currently 1. Bumped only for a breaking change. The translator and your client deploy independently, so drop a frame whose version you don’t recognize rather than half-understand it.
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.

Reading frames off the topic

Register the handler before you connect, so no frame published in the first moments of the session arrives without one.
TypeScript
Registering a second handler for the same topic on the same Room throws. Build a fresh Room per session and register once.

Parsing a frame

Parsing runs on network input inside a stream handler, where a throw takes out your read chain rather than surfacing anywhere useful. Treat malformed JSON, a non-object, an unknown kind, and a future v all as “not a frame I can read”.
TypeScript

Folding frames into a transcript

The two kinds compose differently, and it matters: 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 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.

Rendering captions

The playground’s transcript panel is a good default, and each of its choices comes from a property of the stream:
  • One row per utterance, keyed by segment id — the translation as the headline, the speech it came from underneath once it settles.
  • Label the direction per row, as EN → ZH. Both directions land in one list, and with two Latin-script languages the text itself won’t say which way round a line goes. Until the segment frame lands the source is unknown, so render … → ZH and fill the left side in.
  • Dim text that isn’t final and show a cursor while it is streaming, so a live caption reads as provisional.
  • Autoscroll only while the reader is at the live edge. Somebody who has scrolled up to re-read something must not be dragged back down by the next utterance.
  • Announce new rows with role="log" and aria-live="polite", and set aria-busy on rows that are still streaming so a screen reader can tell a growing caption from a settled one.

Pitfalls

The room holds fewer than two languages. Either a second participant has to join with a different language, or one connection must mint its token with two — {"languages": ["en", "zh"]}. A single-language room stays monolingual and untranslated.
Three different causes, and the message says which:
  • The room name is already in use by a live room (on POST /v1/rooms).
  • The token’s language count contradicts the room’s streaming mode — one language in a mono room, or two in a dual one.
  • The token would introduce a third language. This check is against who is connected right now, so once a speaker leaves, their language frees up and a different one is admitted.
Its first join token inferred it. A room with no declared mode takes the mode implied by that token’s language count, whether or not anyone connects with it, and nothing changes it afterwards. Create a new room, and pass streaming_mode at creation so the mode comes from your intent rather than from a token.
You are appending segment text to the accumulated partials. A segment replaces, it never extends.
Your reads run concurrently. Chain them, as shown above.
Clear the transcript explicitly when the token changes. A fresh token is a fresh session, and state that outlives the connection won’t reset itself.
Disconnecting participants stops their metering — best effort, as each one leaves — but it does not end the room. Call DELETE /v1/rooms/{room_id} when the call is over rather than leaving the room to end on its own.

See it working

Translation Playground, in the Glot dashboard, runs this whole flow in the browser — create a room, connect a mic, and watch partial and segment frames land in the live transcript.