# KolWrite API — complete reference > Speech-to-text REST API at https://app.kolwrite.com. Authenticate with an `x-api-key` header, start an asynchronous job with `POST /transcribe`, read it with `GET /transcribe/{jobId}`. Last updated 2026-10-08. Human reference: https://console.kolwrite.com/public_api_docs. OpenAPI: https://console.kolwrite.com/openapi.json. ## Authentication Every request except `GET /health` and `GET /health/history` needs your API key: ``` x-api-key: YOUR_API_KEY ``` Create an account and generate keys at https://console.kolwrite.com/auth (a person signs up, then generates a key on the API Keys page). Keep the key on the server side. ## Quickstart 1. Start a job: ```bash curl https://app.kolwrite.com/transcribe \ -H "x-api-key: $KOLWRITE_API_KEY" \ -H "Content-Type: application/json" \ -d '{"audio_url": "https://example.com/meeting.m4a", "model": "oracle", "language": "auto", "include_diarization": true, "output_format": "json"}' ``` Response: `{"jobId": "34c63d45-4cb5-473b-bc60-25c175e69c57"}` 2. Poll until `status` is `COMPLETED` or `FAILED`: ```bash curl https://app.kolwrite.com/transcribe/JOB_ID \ -H "x-api-key: $KOLWRITE_API_KEY" ``` ```json { "jobId": "34c63d45-4cb5-473b-bc60-25c175e69c57", "status": "COMPLETED", "result": { "language": "en", "segments": [ { "start": 0.5, "end": 3.2, "text": "Hello everyone, welcome to today's meeting.", "speaker": "Speaker 1", "words": [{ "start": 0.5, "end": 0.8, "word": "Hello" }] } ] } } ``` ## POST /upload Turns a local file into an `audio_url`. Inline Base64 (files up to 4 MiB = 4,194,304 bytes of decoded media): ```json { "filename": "clip.mp3", "contentType": "audio/mpeg", "content": "" } ``` The response includes `fileUrl`. `contentType` is optional (inferred from the filename). Send raw Base64 without a `data:...;base64,` prefix. Direct storage upload (files up to 100 MiB = 104,857,600 bytes): ```json { "filename": "meeting.mp3", "contentType": "audio/mpeg" } ``` The response includes `uploadUrl`, `uploadFields`, `fileUrl` and `uploadExpiresIn`. POST the file to `uploadUrl` as multipart form data with every field of `uploadFields` exactly as returned plus `file`; do not send your API key or another authorization header to the storage URL. The signed form is valid for 60 minutes; the returned read URL is valid for 24 hours. Then call `POST /transcribe` with `audio_url` set to `fileUrl`. ```javascript // Local file up to 100 MiB: ask for a signed form, upload, then transcribe. const upload = await fetch(`${API}/upload`, { method: "POST", headers, body: JSON.stringify({ filename: "meeting.mp3", contentType: "audio/mpeg" }), }).then((response) => response.json()); const form = new FormData(); for (const [name, value] of Object.entries(upload.uploadFields)) form.append(name, value); form.append("file", file, file.name); // no x-api-key on this request await fetch(upload.uploadUrl, { method: "POST", body: form }); // then POST /transcribe with { audio_url: upload.fileUrl, model: "oracle" } ``` ## POST /transcribe Submits audio or video for asynchronous transcription. JSON body: | Parameter | Type | Required | Default | Notes | | --- | --- | --- | --- | --- | | `audio_url` | string | yes | — | Public URL of the audio or video file. For local files, use `fileUrl` from `POST /upload`. | | `language` | string | no | `"auto"` | Language code hint such as `he`, `en`, `es` (see the language table). Omit or `auto` to detect. | | `model` | string | no | `"core"` | `base`, `oracle_mini`, `oracle`, `oracle_pro`, `oracle_max`; legacy `core`, `edge`. Always set it explicitly. | | `translate_to` | string | no | — | Target language code. Oracle family only (`oracle_mini`, `oracle`, `oracle_pro`, `oracle_max`). | | `output_format` | string | no | `"json"` | `json` (segments, timestamps, words), `txt` (plain text), `srt` (SubRip subtitles). | | `word_timestamps` | boolean | no | `true` | Word-level timings in JSON and SRT where the model supports alignment. Plain Oracle Mini returns segment-level timestamps. | | `include_diarization` | boolean | no | `false` | Label speakers (`speaker` on each segment). Included in the price. | | `pro_diarization` | boolean | no | `false` | Premium diarization model for hard audio; adds 0.20 credits per audio hour. | | `num_speakers` | integer | no | auto | Expected speaker count; used only with `include_diarization`. | | `channel_speaker_separation` | string | no | `"off"` | `standard` (turn-taking) or `overlap` (overlapping speech) for 2–8 channel audio. Cannot be combined with diarization. | | `channel_labels` | object | no | — | Zero-based channel map, e.g. `{"0":"Agent","1":"Customer"}`; labels are returned on each segment and word. | | `webhook_url` | string | no | — | Public HTTPS URL that receives a POST when the job completes or fails. | | `webhook_secret` | string | no | — | Signs webhook payloads (`X-KolWrite-Signature: sha256=`). | Response: `{"jobId": ""}`. Example with diarization: ```json { "audio_url": "https://example.com/meeting-audio.mp3", "model": "oracle", "include_diarization": true, "num_speakers": 3 } ``` Example with translation (Oracle models only): ```json { "audio_url": "https://example.com/interview-he.m4a", "model": "oracle", "language": "he", "translate_to": "en" } ``` Example with channel separation (stereo call, one speaker per channel): ```json { "audio_url": "https://example.com/stereo-call.wav", "model": "oracle", "channel_speaker_separation": "overlap", "channel_labels": {"0": "Agent", "1": "Customer"} } ``` ## GET /transcribe/{jobId} | Field | Type | Description | | --- | --- | --- | | `jobId` | string | The job ID. | | `status` | string | `PENDING` (queued), `IN_PROGRESS` (processing), `COMPLETED` (result ready), `FAILED` (see `error`). | | `result` | object or string | Present when `COMPLETED`. JSON output is an object with `language` and `segments`; TXT and SRT are strings. | | `error` | string | Present when `FAILED`. | Segment fields (JSON): `start` and `end` in seconds, `text`, `speaker` (with diarization; e.g. "Speaker 1"), `words` (each `start`, `end`, `word`). With channel separation, segments and words carry their channel and the `channel_labels` label. ## Webhooks Set `webhook_url` to receive `transcription.completed` and `transcription.failed` events. Return any 2xx to acknowledge. Delivery is at least once and failures are retried; retries reuse the delivery ID. Headers: `X-KolWrite-Event`, `X-KolWrite-Job-Id`, `X-KolWrite-Delivery-Id`, `X-KolWrite-Timestamp`, and `X-KolWrite-Signature` (`sha256=`, HMAC-SHA256 of `timestamp + "." + raw_request_body` with `webhook_secret`) when a secret is set. ```json { "event": "transcription.completed", "jobId": "34c63d45-4cb5-473b-bc60-25c175e69c57", "status": "COMPLETED", "createdAt": "2026-07-10T09:30:00+00:00", "model": "base", "output_format": "json", "result_available": true, "result_omitted": false, "result_size_bytes": 12480, "result": { "language": "en", "segments": [] } } ``` For `json` the webhook `result` is an object; TXT and SRT arrive as strings. Large results may set `result_omitted: true`: fetch them with `GET /transcribe/{jobId}`, which stays the source of truth. Failed events include `error`. ## Speaker diarization and channel separation - `include_diarization: true` labels speakers on each segment ("Speaker 1", "Speaker 2", ...). `num_speakers` sets the expected count; omit it to auto-detect. - `pro_diarization: true` uses a premium diarization model (+0.20 credits per audio hour). - `channel_speaker_separation`: `standard` for turn-taking speakers (bills one model rate), `overlap` for overlapping speech (bills the model rate for the greater of the recording length or total detected speech across channels). Needs 2–8 channels; cannot be combined with diarization. - Oracle Mini enables timestamp alignment automatically in any speaker mode. ## Output formats - `json`: segments with timestamps, optional speakers and words. - `txt`: plain text, with speaker labels when diarization is on. - `srt`: SubRip subtitles for video players and subtitle editors. ## Errors and status - Job-level failures: `status: "FAILED"` with an `error` message. - Service health: `GET https://app.kolwrite.com/health` (no key) returns API, job-intake and processing state; 200 means operational or degraded, 503 means unknown or unavailable. `GET /health/history?period=24h|7d|30d` returns availability history. Live page: https://console.kolwrite.com/status. ## Models and prices | Model | `model` value | Credits / audio hour | US$ / audio hour | US$ / minute | Translation (`translate_to`) | Use it for | | --- | --- | --- | --- | --- | --- | --- | | Base | `base` | 0.40 | US$0.48 | US$0.008 | no | Low-cost transcription, word timestamps and speaker separation for clear audio and high volume. | | Oracle Mini | `oracle_mini` | 0.80 | US$0.96 | US$0.016 | yes | Fast Oracle tier; the lowest-priced model that can translate (translate_to). | | Oracle | `oracle` | 1.20 | US$1.44 | US$0.024 | yes | Production transcription and multilingual work, with translation. | | Oracle Pro | `oracle_pro` | 1.40 | US$1.68 | US$0.028 | yes | Premium Oracle quality for demanding production audio. | | Oracle Max | `oracle_max` | 3.00 | US$3.60 | US$0.060 | yes | Maximum quality for difficult or high-stakes audio. | | Core (legacy) | `core` | 1.00 | US$1.20 | US$0.020 | no | Legacy. Kept for existing integrations; it is the default when model is omitted. | | Edge (legacy) | `edge` | 1.20 | US$1.44 | US$0.024 | no | Legacy. Previous-generation higher-accuracy model for existing integrations. | Usage is metered per second of audio with no minimum charge. 1 credit = US$1.20. Regular speaker diarization and standard channel separation are included; Pro diarization adds 0.20 credits per audio hour (US$0.24); overlap channel separation bills the model rate for the greater of the recording length or the total detected speech across channels. ## Languages Send any of these codes as `language` or `translate_to`. Omit `language` to auto-detect. | Language | Native name | `language` / `translate_to` code | | --- | --- | --- | | Afrikaans | Afrikaans | `af` | | Albanian | shqip | `sq` | | Amharic | አማርኛ | `am` | | Arabic | العربية | `ar` | | Armenian | հայերեն | `hy` | | Assamese | অসমীয়া | `as` | | Azerbaijani | azərbaycan | `az` | | Bashkir | башҡорт | `ba` | | Basque | euskara | `eu` | | Belarusian | беларуская | `be` | | Bengali | বাংলা | `bn` | | Bosnian | bosanski | `bs` | | Breton | brezhoneg | `br` | | Bulgarian | български | `bg` | | Burmese | မြန်မာ | `my` | | Catalan | català | `ca` | | Chinese | 中文 | `zh` | | Croatian | hrvatski | `hr` | | Czech | čeština | `cs` | | Danish | dansk | `da` | | Dutch | Nederlands | `nl` | | English | English | `en` | | Estonian | eesti | `et` | | Faroese | føroyskt | `fo` | | Finnish | suomi | `fi` | | French | français | `fr` | | Galician | galego | `gl` | | Georgian | ქართული | `ka` | | German | Deutsch | `de` | | Greek | Ελληνικά | `el` | | Gujarati | ગુજરાતી | `gu` | | Haitian Creole | Kreyòl ayisyen | `ht` | | Hausa | Hausa | `ha` | | Hawaiian | ʻŌlelo Hawaiʻi | `haw` | | Hebrew | עברית | `he` | | Hindi | हिन्दी | `hi` | | Hungarian | magyar | `hu` | | Icelandic | íslenska | `is` | | Indonesian | Bahasa Indonesia | `id` | | Italian | italiano | `it` | | Japanese | 日本語 | `ja` | | Javanese | Basa Jawa | `jw` | | Kannada | ಕನ್ನಡ | `kn` | | Kazakh | қазақ тілі | `kk` | | Khmer | ខ្មែរ | `km` | | Korean | 한국어 | `ko` | | Lao | ລາວ | `lo` | | Latin | Latin | `la` | | Latvian | latviešu | `lv` | | Lingala | lingála | `ln` | | Lithuanian | lietuvių | `lt` | | Luxembourgish | Lëtzebuergesch | `lb` | | Macedonian | македонски | `mk` | | Malagasy | Malagasy | `mg` | | Malay | Bahasa Melayu | `ms` | | Malayalam | മലയാളം | `ml` | | Maltese | Malti | `mt` | | Maori | Māori | `mi` | | Marathi | मराठी | `mr` | | Mongolian | монгол | `mn` | | Nepali | नेपाली | `ne` | | Norwegian | norsk bokmål | `no` | | Nynorsk | norsk nynorsk | `nn` | | Occitan | occitan | `oc` | | Pashto | پښتو | `ps` | | Persian | فارسی | `fa` | | Polish | polski | `pl` | | Portuguese | português | `pt` | | Punjabi | ਪੰਜਾਬੀ | `pa` | | Romanian | română | `ro` | | Russian | русский | `ru` | | Sanskrit | संस्कृतम् | `sa` | | Serbian | српски | `sr` | | Shona | chiShona | `sn` | | Sindhi | سنڌي | `sd` | | Sinhala | සිංහල | `si` | | Slovak | slovenčina | `sk` | | Slovenian | slovenščina | `sl` | | Somali | Soomaali | `so` | | Spanish | español | `es` | | Sundanese | Basa Sunda | `su` | | Swahili | Kiswahili | `sw` | | Swedish | svenska | `sv` | | Tagalog | Tagalog | `tl` | | Tajik | тоҷикӣ | `tg` | | Tamil | தமிழ் | `ta` | | Tatar | татар | `tt` | | Telugu | తెలుగు | `te` | | Thai | ไทย | `th` | | Tibetan | བོད་སྐད་ | `bo` | | Turkish | Türkçe | `tr` | | Turkmen | türkmen dili | `tk` | | Ukrainian | українська | `uk` | | Urdu | اردو | `ur` | | Uzbek | o‘zbek | `uz` | | Vietnamese | Tiếng Việt | `vi` | | Welsh | Cymraeg | `cy` | | Yiddish | ייִדיש | `yi` | | Yoruba | Èdè Yorùbá | `yo` | | Zulu | isiZulu | `zu` | ## Python ```python import os, time, requests API = "https://app.kolwrite.com" HEADERS = {"x-api-key": os.environ["KOLWRITE_API_KEY"]} job = requests.post(f"{API}/transcribe", headers=HEADERS, json={ "audio_url": "https://example.com/meeting.m4a", "model": "oracle", # always set it: the default is the legacy "core" "language": "auto", # or a code such as "he", "ar", "es" "include_diarization": True, "output_format": "json", }, timeout=30).json() while True: result = requests.get(f"{API}/transcribe/{job['jobId']}", headers=HEADERS, timeout=30).json() if result["status"] in ("COMPLETED", "FAILED"): break time.sleep(5) if result["status"] == "FAILED": raise RuntimeError(result.get("error")) for segment in result["result"]["segments"]: print(f'{segment["start"]:.1f}s {segment.get("speaker", "")}: {segment["text"]}') ``` ## JavaScript ```javascript const API = "https://app.kolwrite.com"; const headers = { "x-api-key": process.env.KOLWRITE_API_KEY, "Content-Type": "application/json" }; const { jobId } = await fetch(`${API}/transcribe`, { method: "POST", headers, body: JSON.stringify({ audio_url: "https://example.com/meeting.m4a", model: "oracle", // always set it: the default is the legacy "core" language: "auto", include_diarization: true, output_format: "json", }), }).then((response) => response.json()); let job; do { await new Promise((resolve) => setTimeout(resolve, 5000)); job = await fetch(`${API}/transcribe/${jobId}`, { headers }).then((response) => response.json()); } while (job.status === "PENDING" || job.status === "IN_PROGRESS"); if (job.status === "FAILED") throw new Error(job.error); console.log(job.result.segments.map((segment) => segment.text).join("\n")); ```