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
Room
Room
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).Participant
Participant
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.Languages
Languages
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.Translator
Translator
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
- Declare it
- Let Glot infer it
Pass 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.
streaming_mode when you create the room. Use this when you already know the shape of the call.cURL
JSON
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 with409. The message names the mode and the count it expects:
409 Conflict
dual room takes exactly one language per token, and a mono room takes two on every connection.
Start a session
1
Create the room
Languages are not part of this call. Pass The room needs credit: a request against an organization whose balance is at or below zero returns
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
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
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 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
id — the persisted UUID — not the name tokens are minted against.cURL
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 amono 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
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.
Live transcription
The translator publishes one JSON frame per text stream on the topicglot.transcript, over the same room connection. There is no separate transcription endpoint or socket to open.
Two kinds of frame
The model streams a translation before it confirms it, so an utterance arrives in two forms. Both carry asegment id, which is what ties them together.
- partial
- segment
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 unknownkind, 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
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
segmentid — 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 thesegmentframe lands the source is unknown, so render… → ZHand fill the left side in. - Dim text that isn’t
finaland show a cursor while it isstreaming, 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"andaria-live="polite", and setaria-busyon rows that are still streaming so a screen reader can tell a growing caption from a settled one.
Pitfalls
Nothing is translated and no translator appears
Nothing is translated and no translator appears
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.A token request returns 409
A token request returns 409
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
monoroom, or two in adualone. - 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.
A room ended up in the wrong streaming mode
A room ended up in the wrong streaming mode
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.Captions duplicate themselves
Captions duplicate themselves
You are appending
segment text to the accumulated partials. A segment replaces, it never extends.Chunks from two utterances interleave
Chunks from two utterances interleave
Your reads run concurrently. Chain them, as shown above.
A new session opens showing the last call's captions
A new session opens showing the last call's captions
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.
The room is still open after everyone left
The room is still open after everyone left
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.