# Clarion: API documentation

The Clarion API turns Uzbek speech into punctuated text with word-level timestamps and sound events: files over REST, live audio over WebSocket. It is OpenAI-compatible, so with the OpenAI SDK only `base_url` changes.

Base URL: `https://api.uzasr.uz/v1`

## Authentication

Send the key in a header with every request.

Keys look like `uzasr_live_…` and are shown once. Keep yours on the server, never in a browser or client app.

```http
Authorization: Bearer uzasr_live_…
```

## Quickstart

Transcribe a file with a single request.

```python
import os

from openai import OpenAI

client = OpenAI(
    base_url="https://api.uzasr.uz/v1",
    api_key=os.environ["UZASR_API_KEY"],
)

result = client.audio.transcriptions.create(
    model="uzasr-large",
    file=open("call.mp3", "rb"),
    response_format="verbose_json",
    timestamp_granularities=["word"],
)
print(result.text)
```

```javascript
import fs from "node:fs"
import OpenAI from "openai"

const client = new OpenAI({
  baseURL: "https://api.uzasr.uz/v1",
  apiKey: process.env.UZASR_API_KEY,
})

const result = await client.audio.transcriptions.create({
  model: "uzasr-large",
  file: fs.createReadStream("call.mp3"),
  response_format: "verbose_json",
  timestamp_granularities: ["word"],
})
console.log(result.text)
```

## Synchronous transcription

Files up to 10 minutes. The response comes back in the same request.

`POST /v1/audio/transcriptions`

| Parameter | Type | Description |
| --- | --- | --- |
| `file` | file | Audio or video: wav, mp3, m4a, ogg, flac, mp4, webm. |
| `model` | string | `uzasr-large` or `uzasr-base`, or an exact version. |
| `response_format` | string | `json`, `text` or `verbose_json`. Defaults to `json`. |
| `timestamp_granularities[]` | string | `word`: word-level timestamps, only with `verbose_json`. |
| `language` | string | Optional. Only `uz` is supported. |

## Response format

`verbose_json` with word-level timestamps and sound events.

| Parameter | Type | Description |
| --- | --- | --- |
| `text` | string | Punctuated text in Latin script. |
| `words[]` | object | `word`, `start`, `end`: the word and its boundaries in seconds. |
| `segments[]` | object | Recording segments with boundaries and text. |
| `events[]` | object | `label`, `start`, `end`, `score`. Labels: `laughter`, `applause`, `music`. Ignore unknown labels. |

```json
{
  "task": "transcribe",
  "language": "uz",
  "duration": 28.0,
  "text": "Oyoqdagi yechish bilan shunaqa ovoz chiqaradiki, …",
  "segments": [
    { "id": 0, "start": 0.0, "end": 9.8, "text": "Oyoqdagi yechish bilan …" }
  ],
  "words": [
    { "word": "Oyoqdagi", "start": 0.12, "end": 0.64 }
  ],
  "events": [
    { "label": "laughter", "start": 2.28, "end": 2.52, "score": 0.557 },
    { "label": "applause", "start": 19.28, "end": 21.04, "score": 0.659 }
  ]
}
```

The `X-Uzasr-Processing-Seconds` header contains pure GPU processing time, excluding queueing and startup.

## Async jobs

Files up to 3 hours. Create a job and poll its status.

`POST /v1/jobs` · `GET /v1/jobs/{id}`

Retrying with the same `Idempotency-Key` returns the original job. Statuses: `queued`, `succeeded`, `failed`.

```python
import os
import time

import requests

API = "https://api.uzasr.uz/v1"
headers = {"Authorization": f"Bearer {os.environ['UZASR_API_KEY']}"}

job = requests.post(
    f"{API}/jobs",
    headers={**headers, "Idempotency-Key": "call-2026-10-21-0042"},
    files={"file": open("meeting.mp3", "rb")},
    data={"model": "uzasr-large"},
).json()

while job["status"] == "queued":
    time.sleep(5)
    job = requests.get(f"{API}/jobs/{job['id']}", headers=headers).json()

print(job["result"]["text"])
```

```javascript
import { openAsBlob } from "node:fs"

const API = "https://api.uzasr.uz/v1"
const headers = { Authorization: `Bearer ${process.env.UZASR_API_KEY}` }

const form = new FormData()
form.append("file", await openAsBlob("meeting.mp3"), "meeting.mp3")
form.append("model", "uzasr-large")

let job = await fetch(`${API}/jobs`, {
  method: "POST",
  headers: { ...headers, "Idempotency-Key": "call-2026-10-21-0042" },
  body: form,
}).then((res) => res.json())

while (job.status === "queued") {
  await new Promise((resolve) => setTimeout(resolve, 5000))
  const poll = await fetch(`${API}/jobs/${job.id}`, { headers })
  job = await poll.json()
}

console.log(job.result.text)
```

## Realtime mode

WebSocket: stream PCM in, receive text events out.

`WS /v1/realtime?model=uzasr-large`

| Event | Direction | Description |
| --- | --- | --- |
| `ready` | ← server | Session opened, with the exact model version. |
| `partial` | ← server | The current phrase. The `stable` field won't change; the rest may be refined. |
| `final` | ← server | Phrase finalized after a 0.8 s pause, with punctuation. |
| `event` | ← server | A sound event, 1–1.5 s after it ends. |
| `done` | ← server | Session closed, with `audio_seconds` for billing. |
| `binary` | → client | PCM s16le, 16 kHz, mono. Recommended frame size: 100 ms. |
| `stop` | → client | `{"type":"stop"}`: finalize the last phrase and close the session. |

```text
wss://api.uzasr.uz/v1/realtime?model=uzasr-large

← { "type": "ready" }
→ <binary PCM s16le 16 kHz, 100 ms>
← { "type": "partial", "utterance": 0, "start": 0.0,
    "text": "Assalomu alaykum qanday", "stable": "Assalomu alaykum" }
← { "type": "final", "utterance": 0, "start": 0.0, "end": 2.4,
    "text": "Assalomu alaykum, qanday yordam bera olaman?" }
→ { "type": "stop" }
← { "type": "done", "audio_seconds": 2.9 }
```

Browsers can't send headers over WebSocket: connect from your server and proxy the stream.

## Models

An alias always points to the current version.

`GET /v1/models`

| Model | Parameters | WER | When to use |
| --- | --- | --- | --- |
| `uzasr-large` | 600M | 8.1% | Default, maximum accuracy |
| `uzasr-base` | 220M | 9.4% | High volumes, lower cost |

- [600M model on Hugging Face](https://huggingface.co/rustam1221/UzASR-CTC-600M)
- [220M model on Hugging Face](https://huggingface.co/rustam1221/UzASR-CTC-220M)

## Errors

A single OpenAI-compatible format.

| HTTP | code | What happened |
| --- | --- | --- |
| 400 | `unsupported_parameter` | Unknown form parameter. |
| 400 | `unsupported_language` | `language` is set to something other than `uz`. |
| 400 | `audio_unreadable` | Couldn't decode the audio. |
| 400 | `audio_too_long` | The recording exceeds the endpoint limit. Use `/v1/jobs`. |
| 401 | `invalid_api_key` | The key is missing, invalid or revoked. |
| 404 | `model_not_found` | No such model. See `GET /v1/models`. |
| 413 | `file_too_large` | The file exceeds the size limit. |
| 429 | `concurrency_limit_exceeded` | Too many concurrent requests. See `Retry-After`. |
| 503 | `model_unavailable` | The model is starting up or under maintenance. Try again in a minute. |
| 504 | `timeout` | Transcription timed out. For long files, use `/v1/jobs`. |

```json
{
  "error": {
    "type": "invalid_request_error",
    "code": "unsupported_language",
    "message": "Only 'uz' is supported"
  }
}
```

## Limits

Default values. Configurable for enterprise customers.

| Limit | Value |
| --- | --- |
| Synchronous request | up to 10 minutes of audio |
| Async job | up to 3 hours of audio |
| Concurrent requests per key | 8 |
| Realtime session | up to 5 minutes |
| Job result retention | 7 days |
