> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tiro.ooo/llms.txt
> Use this file to discover all available pages before exploring further.

# Quickstart

> Create a realtime transcription session, stream PCM audio over WebSocket, read transcripts and translations, and close the session, with a Python example.

Enterprise The Realtime Transcription API is available to Enterprise plan customers on request. Tiro must enable the API for your workspace before you can follow this guide.

In this guide, you stream one audio file at real-time pace, print the transcripts and translations in your terminal, and close the session.

## Before you begin

* A workspace with the Realtime Transcription API enabled. See the requirements in [Overview](https://docs.tiro.ooo/en/developers/realtime-transcription/overview.md#who-can-use).
* An API key bound to that workspace. If the key has scopes, it needs `realtime_transcription:write`. Put the key in the `TIRO_API_KEY` environment variable.
* Python 3.9 or later and the `websocket-client` package. Install it with `pip install websocket-client`.
* `ffmpeg` to convert your audio file to PCM.

**1. Convert audio to 16 kHz mono PCM**

The Realtime Transcription API accepts only 16,000 Hz, mono, signed 16-bit little-endian PCM. The data must be raw, with no header. Convert any audio file to this format with `ffmpeg`.

```bash theme={"system"}
ffmpeg -i meeting.m4a -ac 1 -ar 16000 -f s16le -acodec pcm_s16le meeting.pcm
```

This format is 32,000 bytes per second. Use the same format when you stream microphone input.

**2. Create a session**

Create the session on your server. This example transcribes Korean and translates it to English. If you do not need a translation, leave out `translationLocale`.

```bash cURL theme={"system"}
curl -X POST https://api.tiro.ooo/v1/external/realtime-transcriptions \
  -H "Authorization: Bearer $TIRO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "transcriptionLocale": "ko_KR",
    "translationLocale": "en_US"
  }'
```

```python Python theme={"system"}
import json
import os
import urllib.request

request = urllib.request.Request(
    "https://api.tiro.ooo/v1/external/realtime-transcriptions",
    data=json.dumps({"transcriptionLocale": "ko_KR", "translationLocale": "en_US"}).encode(),
    headers={
        "Authorization": f"Bearer {os.environ['TIRO_API_KEY']}",
        "Content-Type": "application/json",
    },
    method="POST",
)
with urllib.request.urlopen(request) as response:
    session = json.load(response)
print(session["id"], session["wsUrl"])
```

On success, you receive `201 Created` with the session.

```json theme={"system"}
{
  "id": "Rt7Yq2PbX9cDk",
  "workspaceGuid": "Wk8sQ2mLx1aB7",
  "status": "OPEN",
  "endReason": null,
  "wsUrl": "wss://api.tiro.ooo/v1/external/realtime-transcriptions/Rt7Yq2PbX9cDk/stream",
  "streamToken": "rtt_5Hq3...",
  "maxDurationMs": 10800000,
  "audioDurationMs": 0,
  "transcriptionLocale": "ko_KR",
  "translationLocale": "en_US",
  "apiKeyId": "tk_8f2a91c0d3e4",
  "createdAt": "2026-11-05T09:00:00Z",
  "endedAt": null
}
```

You receive `streamToken` only in this response. Keep it safe, because you use the same token to reconnect while the session is `OPEN`. Use `wsUrl` exactly as returned. Do not build it yourself.

**3. Connect over WebSocket**

On the client, connect to `wsUrl` and put `streamToken` in the `Authorization` header. You cannot send the token in a query string or a cookie.

```python theme={"system"}
import websocket

ws = websocket.create_connection(
    session["wsUrl"],
    header=[f"Authorization: Bearer {session['streamToken']}"],
    suppress_origin=True,
)
print(ws.recv())
```

After you connect, the first message is `session.started`. It usually arrives within about a second, and rarely takes tens of seconds. You can start sending audio before it arrives.

```json theme={"system"}
{ "type": "session.started", "id": "Rt7Yq2PbX9cDk", "resumed": false, "audioOffsetMs": 0 }
```

**4. Send audio and read results**

Split the audio into 3,200-byte (100 ms) chunks and send each as a binary frame. Send recorded files at real-time pace too. If you get more than 60 seconds ahead of real time, Tiro closes the connection with close `4029`.

Results arrive as `transcript` and `translation` text messages. An `isFinal: false` message replaces the whole previous text for the same `segmentId`. When `isFinal: true` arrives, that segment is final.

```json theme={"system"}
{ "type": "transcript", "segmentId": "Ab3kZ9qLm2XyW-1-17", "isFinal": true, "text": "오늘 회의는 일정 점검부터 하겠습니다.", "locale": "ko_KR" }
{ "type": "translation", "translationId": "t3-9", "isFinal": true, "locale": "en_US", "text": "Today's meeting will start with a schedule review.", "sourceSegmentId": null }
```

**5. Close the session**

After the last audio frame, send `{"type": "session.end"}`. Tiro sends all remaining results with `isFinal: true`, sends `session.ended`, and closes the connection with close `1000`. Keep the connection open until you receive `session.ended`. Sending the remaining results takes up to 30 seconds.

```json theme={"system"}
{ "type": "session.ended", "id": "Rt7Yq2PbX9cDk", "endReason": "CLIENT_REQUESTED", "audioDurationMs": 3605120, "endedAt": "2026-11-05T10:01:30Z" }
```

If you cannot use the WebSocket, close the session from your server with `POST /v1/external/realtime-transcriptions/{id}/end`.

> [!WARNING]
>
> If you close the WebSocket connection without ending the session, the session stays `OPEN`. It holds one of the workspace's 10 concurrent session slots until it ends automatically after 15 minutes without audio.

## Full example

The script below combines the steps above. It streams a PCM file at real-time pace, prints the results, and closes the session. If the connection drops, it does not reconnect and ends the session over REST. To add reconnection, follow the [reconnection rules](https://docs.tiro.ooo/en/developers/realtime-transcription/protocol.md#reconnect).

> [accordion] View the full stream\_pcm.py code
>
> ```python stream_pcm.py theme={"system"}
> import json
> import os
> import ssl
> import sys
> import time
> import urllib.request
>
> import websocket
>
> BASE_URL = "https://api.tiro.ooo"
> API_KEY = os.environ["TIRO_API_KEY"]
> FRAME_BYTES = 3200
> FRAME_SECONDS = 0.1
>
>
> def rest(method, path, body=None):
>     request = urllib.request.Request(
>         f"{BASE_URL}{path}",
>         data=json.dumps(body).encode() if body is not None else None,
>         headers={"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"},
>         method=method,
>     )
>     with urllib.request.urlopen(request) as response:
>         return json.load(response)
>
>
> def handle(message):
>     kind = message.get("type")
>     if kind in ("transcript", "translation"):
>         marker = "FINAL  " if message["isFinal"] else "partial"
>         print(f"{marker} {kind} ({message.get('locale')}) {message['text']}")
>     elif kind == "error":
>         print(f"error reason={message['reason']} retryable={message['retryable']} {message['message']}")
>         if message["fatal"]:
>             raise ConnectionError(message["reason"])
>     elif kind == "session.ended":
>         print(f"session.ended endReason={message['endReason']} audioDurationMs={message['audioDurationMs']}")
>
>
> def recv_json(ws):
>     raw = ws.recv()
>     if not raw:
>         raise ConnectionError("connection closed")
>     return json.loads(raw)
>
>
> def drain(ws):
>     ws.settimeout(0.0)
>     try:
>         while True:
>             handle(recv_json(ws))
>     except (websocket.WebSocketTimeoutException, BlockingIOError, ssl.SSLWantReadError):
>         pass
>     finally:
>         ws.settimeout(None)
>
>
> def stream(session, pcm):
>     ws = websocket.create_connection(
>         session["wsUrl"],
>         header=[f"Authorization: Bearer {session['streamToken']}"],
>         suppress_origin=True,
>     )
>     started_at = time.monotonic()
>     sent_seconds = 0.0
>     for offset in range(0, len(pcm), FRAME_BYTES):
>         ws.send_binary(pcm[offset : offset + FRAME_BYTES])
>         sent_seconds += FRAME_SECONDS
>         drain(ws)
>         wait = started_at + sent_seconds - time.monotonic()
>         if wait > 0:
>             time.sleep(wait)
>
>     ws.send(json.dumps({"type": "session.end"}))
>     ws.settimeout(60)
>     while True:
>         message = recv_json(ws)
>         handle(message)
>         if message.get("type") == "session.ended":
>             break
>     ws.close()
>
>
> def main():
>     with open(sys.argv[1], "rb") as file:
>         pcm = file.read()
>     session = rest("POST", "/v1/external/realtime-transcriptions", {"transcriptionLocale": "ko_KR", "translationLocale": "en_US"})
>     print(f"created id={session['id']}")
>     try:
>         stream(session, pcm)
>     except (websocket.WebSocketException, ConnectionError, OSError) as error:
>         print(f"websocket failed: {error}. Ending the session over REST.")
>         rest("POST", f"/v1/external/realtime-transcriptions/{session['id']}/end")
>         sys.exit(1)
>
>
> if __name__ == "__main__":
>     main()
> ```

Run it with the path to a PCM file as the argument.

```bash theme={"system"}
python stream_pcm.py meeting.pcm
```

## Next steps

> [card] [WebSocket protocol](https://docs.tiro.ooo/en/developers/realtime-transcription/protocol.md)
>
> Message fields, errors, reconnection, and close codes.

> [card] [API reference](https://docs.tiro.ooo/api-reference/realtime-transcription/create-realtime-transcription.md)
>
> REST APIs to create, get, list, and end sessions.

Enterprise Plan

This feature is available on the Enterprise plan.  Our sales team will help design the security setup your organization needs.

[Contact sales](https://tiro.ax/en/contact?utm_source=docs\&utm_medium=plan_callout\&utm_campaign=plan_upsell_enterprise\&utm_content=developers-realtime-transcription-quickstart)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.