wsUrl after you create a session. The REST APIs to create, get, list, and end sessions are in the API reference.
How do sessions and connections differ?
A session is the transcription unit you create over REST. A connection is one WebSocket attached to that session. A session has only two states,OPEN and ENDED. While the session is OPEN, a connection can drop and attach again.
- Once a session is
ENDED, you cannot reopen it. To keep transcribing, create a new session. - When a connection drops, the session stays
OPEN. Reconnect with the samewsUrlandstreamToken. - You receive the reason the session ended in
endReason. The values are listed undersession.ended.
Connecting
Connect to thewsUrl from the POST /v1/external/realtime-transcriptions response with the Authorization: Bearer <streamToken> header.
- Use
wsUrlexactly as returned. It currently looks likewss://<host>/v1/external/realtime-transcriptions/{id}/stream, but the host and path can change. - Tiro accepts the token only in the
Authorizationheader. You cannot connect with a query string, a cookie, or an API key. For that reason, the browser WebSocket API, which cannot set headers, cannot connect. - Tiro does not check the Origin header.
- A
streamTokenworks only for its session, and you use the same token to reconnect while the session isOPEN. You receive it only in the session creation response, and no API issues it again. If you lose the token, end the session and create a new one.
error message and then closes the connection. Tiro rejects with an HTTP status only in these three cases.
After accepting, Tiro checks the token, the session status, the API key that issued the token, and the workspace policy. If the policy is turned off or the API key is deleted during a session, Tiro keeps the open connection and rejects the next connection.
Audio frames
Send audio as binary frames and control messages as text frames. Each text frame carries one UTF-8 JSON object.
You can send audio before you receive
session.started. Tiro keeps audio that arrives before transcription is ready and processes it in order.
How is the audio lead calculated?
The audio lead is how far the audio you sent is ahead of the real elapsed time. Tiro measures it for each connection and closes the connection when it exceeds 60 seconds.- Idle time does not build up credit. If you send nothing for a minute and then send two minutes of audio at once, you still exceed 60 seconds.
- A new connection inherits the lead of the previous one. Reconnecting does not reset the limit.
- Above 60 seconds,
retryAfterMsinerror(audio_rate_exceeded) holds the overflow. Wait at least that long, reconnect, and send at real-time pace.
Messages
Every text message has atype field. Lengths and offsets are integer milliseconds with names that end in Ms. Times are ISO-8601 UTC strings with names that end in At. Locales use the Supported locales format, such as ko_KR.
Ordering. Messages for the same
segmentId (transcript) and the same translationId (translation) arrive in the order Tiro sent them. Zero or more isFinal: false messages come first, then one isFinal: true. Tiro does not guarantee order between different segments, or between transcript and translation.
Compatibility. Tiro can add message types and fields. It does not remove fields or change their names, types, or meanings. Ignore, or apply a default to, any unknown type, field, reason, endReason, or status value.
session.end
The client sends this to end the session. You can send it more than once. Tiro ignores every copy after the first. For what happens next, see Ending a session.
session.started
The first message of each connection. It usually arrives in about 1 second, and rarely takes tens of seconds.
transcript
A transcription result.
- When an
isFinal: falsemessage arrives for asegmentId, replace that segment’s whole text. isFinal: truearrives exactly once per segment. After it, no more messages arrive for that segment.- A segment can arrive as
isFinal: trueonly, with noisFinal: falsebefore it. - Tiro decides segment size and does not guarantee it. Segments currently break where an utterance ends.
translation
A translation result. It arrives only when the session set translationLocale.
- The replacement rule matches
transcript. AnisFinal: falsemessage replaces the whole previous text for the sametranslationId, andisFinal: truearrives only once. - Translation pieces do not pair 1:1 with source segments. One piece can span several segments, and one segment can split across several pieces.
- Join the
textof final pieces in arrival order, with no separator, to get the translation. Pieces can break mid-word, so do not add spaces between them. A final piece that contains only whitespace is a word break, so join it as is. - A translation can arrive after its source is final, or before the source’s
isFinal: falsemessage. - In an auto-detect session, Tiro does not translate a segment whose
transcript.localeequalstranslationLocale. In a session with a set language, speech in the target language can also go untranslated.
session.ended
Tells you that the session ended. This is the last message of the session. Tiro closes the connection with close 1000 after it. When you receive it, do not reconnect.
A session that ends while no connection is open has nowhere to send this message. In that case, check the status with
GET /v1/external/realtime-transcriptions/{id}. Connecting to an ended session returns error (session_ended).
error
Reports an error.
Ending a session
Sendsession.end to receive every last result.
1
Send session.end
After the last audio frame, send
{"type": "session.end"}. Tiro ignores audio sent after it.2
Receive the remaining results
Tiro sends all remaining
transcript and translation messages with isFinal: true. Segments and translation pieces that are not final yet are sent as final with their last text.3
Receive session.ended
Tiro sends
session.ended and closes the connection with close 1000.session.ended. Sending the remaining results takes up to 30 seconds, including translations. If it does not finish within 30 seconds, Tiro sends session.ended with the results sent so far and adds incomplete: true.
When a session ends over REST or by a timeout while a connection is open, it ends in the same order. In those cases, audio that reached Tiro but has not started transcription yet may be left out of the results.
If you only close the WebSocket connection, the session stays OPEN and holds one of the workspace’s concurrent session slots. If a connection simply drops, you also do not receive the results that were not final at that moment.
Timeouts
- Text messages do not reset the no-audio timer.
- While muted, you can keep the connection open and stop sending. If you send audio again within 15 minutes, the session continues.
- Tiro can end a session later than the timeout: up to about 1 minute late with a connection, and up to about 3 minutes late with no connection.
Reconnecting
While a session isOPEN, you can reconnect with the same wsUrl and streamToken after a disconnect. Decide whether to reconnect based on the last message you received, not the close code.
- If you received
session.ended, the session is over. Do not reconnect. - If the
errorthat closed the connection (fatal: true) hadretryable: false, do not reconnect. Ignore earlier non-fatal errors (fatal: false, such asinvalid_message) in this decision. - In every other case, reconnect. That covers an
errorwithretryable: trueand a close with no message. Close1012is a server restart, so reconnect right away. For everything else, use backoff.
retryAfterMs is present, wait at least that long. While the session is OPEN, there is no limit on reconnection attempts.
A successful reconnection returns session.started with resumed: true. If the session already ended, you receive error (session_ended, retryable: false).
How do I resend audio from a disconnect?
Tiro never asks for audio again. Audio that was not sent is not part of the session, so the client must buffer it and send it again.- While disconnected, keep up to the most recent 60 seconds of audio. Drop anything older.
- After reconnecting, read
audioOffsetMsfromsession.started. It is the audio length Tiro finished processing, so it can be shorter than what the previous connection actually sent. If a new connection replaces the previous one before Tiro notices that the previous one dropped, the value can be0. - First send the buffered audio from the
audioOffsetMspoint of the session timeline onward, then continue with live audio. This stays within the 60-second audio lead. - Tiro does not check whether resent audio duplicates audio it already received.
- Close
1012arrives with no message, so some frames sent just before it may not have reached Tiro. In that case too, resend the audio afteraudioOffsetMs. - Discard segments and translation pieces that were still
isFinal: falseat the disconnect. Tiro does not send them again on the new connection.
What happens if I connect to the same session twice?
A session has one connection at a time. When a new connection arrives with the same token, Tiro uses the new connection and closes the old one.- The old connection receives
error(connection_replaced,retryable: false) and closes with close4409. Do not reconnect from the old connection’s client. - The new connection receives
session.startedwithresumed: trueand continues the session.
WebSocket errors
Newreason values can appear. For an unknown reason, follow its fatal and retryable values.
Close codes
Use this table when you read logs. Decide on reconnection with the Reconnecting rules and not the close code. New close codes can appear.REST errors
The session REST APIs return errors in the shared format from Error handling. Branch ondetail.reason. For a response with no detail.reason, branch on the HTTP status. Errors for each endpoint are in the API reference.