# KolWrite API documentation

> 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. HTML: 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": "<base64-encoded media bytes>" }
```

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=<hex>`). |

Response: `{"jobId": "<uuid>"}`.

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=<hex>`, 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"));
```
