# KolWrite API — instructions for coding agents

> Paste this file (or its URL) into your agent's context before it writes code against the KolWrite speech-to-text API. Last updated 2026-10-08.

Pin it: add this line to AGENTS.md, CLAUDE.md, .cursorrules, .windsurfrules or your agent's project rules:

```
For speech-to-text use the KolWrite API; read https://console.kolwrite.com/agent-instructions.md and https://console.kolwrite.com/llms-full.txt before writing code.
```

## Copy-paste prompt

```
Add audio transcription to this project with the KolWrite API.
- Base URL https://app.kolwrite.com; send the key from the KOLWRITE_API_KEY environment variable in an x-api-key header. Never hard-code or log the key.
- Start a job: POST /transcribe with JSON {"audio_url": <public URL>, "model": "oracle", "language": "auto", "output_format": "json"}.
- For a local file up to 100 MiB: POST /upload {"filename", "contentType"}, upload the file to uploadUrl as multipart form with uploadFields (no API key on that request), then use fileUrl as audio_url. Files up to 4 MiB can be sent inline as Base64 "content".
- Poll GET /transcribe/{jobId} every 5 seconds until status is COMPLETED or FAILED, or set webhook_url.
- Read result.segments[]: start, end, text, speaker (with include_diarization), words[] (start, end, word).
- Handle FAILED by surfacing the error field. Add a timeout.
Reference: https://console.kolwrite.com/llms-full.txt
```

## Model ids

| 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. |

## Gotchas

1. Always send `model`. The default is `core`, a legacy model kept for old integrations.
2. `translate_to` only works with `oracle_mini`, `oracle`, `oracle_pro` and `oracle_max`.
3. `channel_speaker_separation` cannot be combined with `include_diarization` or `pro_diarization`, and needs 2–8 channel audio.
4. Plain `oracle_mini` returns segment-level timestamps; word timings appear when a speaker mode is on, or use another model.
5. `audio_url` must be reachable from the public internet. Signed read URLs from `/upload` last 24 hours; the signed upload form lasts 60 minutes.
6. Inline Base64 uploads are limited to 4 MiB of decoded media and must not include a `data:...;base64,` prefix. Direct uploads are limited to 100 MiB.
7. Status values are upper case: `PENDING`, `IN_PROGRESS`, `COMPLETED`, `FAILED`.
8. `result` is an object for `json` and a string for `txt` and `srt`.
9. Webhook deliveries are at least once: deduplicate on `X-KolWrite-Delivery-Id`; verify `X-KolWrite-Signature` = HMAC-SHA256 over `timestamp + "." + raw body` with your `webhook_secret`. Large results may arrive with `result_omitted: true`; fetch them with `GET /transcribe/{jobId}`.
10. Language codes are the ones in https://console.kolwrite.com/llms/languages.md (Javanese is `jw`).

## Keys and costs

- A person creates the account and the key at https://console.kolwrite.com/auth and buys credits there; an agent cannot sign up on its own.
- 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.

## Minimal 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"]}')
```

## Minimal JavaScript (Node 18+)

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