# KolWrite API for AI agents

How to let an AI agent (Claude Code, Codex, Cursor, or a script of your own)
turn recordings into transcripts, SRT subtitles and word timings with the
KolWrite platform API at `https://api.kolwrite.com`. A person creates a token
once; the agent reads it from an environment variable and calls plain HTTPS.
No SDK is needed.

This is the platform API with `kol_` tokens. The credits API at
`https://app.kolwrite.com` (header `x-api-key`) is a different API, described at
https://console.kolwrite.com/llms.txt.

## 1. Give the agent a token

- API access is enabled per account; without it, creating or using a token is
  refused.
- A person creates the token in the KolWrite app (**Settings → Account → API
  access → Create token**) and picks the smallest set of scopes. The secret
  (`kol_...`) is shown once.
- Put it in an environment variable and keep it out of prompts, logs and git.

| Task | Scopes |
|---|---|
| Transcribe and export | `files:write`, `jobs:write`, `jobs:read`, `transcripts:read` |
| Also check the credit balance | add `billing:read` |
| OpenAI-compatible endpoints | `ai:use` (spends credits; not part of the app's Automation preset) |
| Create share links | add `shares:write` |

Paste this into `AGENTS.md`, `CLAUDE.md` or `.cursorrules`:

```
## KolWrite platform API
- Base URL https://api.kolwrite.com. The token is in the environment variable KOLWRITE_TOKEN: send it as "Authorization: Bearer $KOLWRITE_TOKEN" and never print it.
- Flow: POST /api/v1/media/upload-session, PUT the bytes to data.upload_url, POST /api/v1/media/complete, POST /api/v1/jobs, poll GET /api/v1/jobs/{job_id} until status is succeeded, then GET /api/v1/jobs/{job_id}/export?format=json
- Spec: https://kolwrite-docs.pages.dev/openapi.json. Guide: https://kolwrite-docs.pages.dev/agents.md
```

## 2. When to use which call

| You want | Call |
|---|---|
| A transcript of a file, with word timings and speakers | upload session, then `POST /api/v1/jobs` (`type: transcription`), then `GET /api/v1/jobs/{job_id}/export?format=json` |
| SRT or VTT subtitles | `GET /api/v1/jobs/{job_id}/export?format=srt` or `format=vtt` (Plus plan or higher) |
| Subtitles in another language | `options.target_language` on the job, then export |
| Plain text | `export?format=text` |
| Text for a short voice clip, no job | `POST /api/v1/ai/openai/audio/transcriptions` |
| Chat, responses, embeddings | `/api/v1/ai/openai/*` ([OpenAI-compatible endpoints](/openai-compatible.md)) |
| The saved, versioned transcript | `GET /api/v1/jobs/{job_id}/artifacts` |
| The credit balance | `GET /api/v1/billing/balance` |
| A public link to a transcript | `POST /api/v1/shares` |
| Which model profiles exist | `GET /api/v1/models` |

## 3. Transcribe a file in Python

```python
import json, mimetypes, os, time
import requests

BASE = "https://api.kolwrite.com"
TOKEN = os.environ["KOLWRITE_TOKEN"]


def call(method, path, **kw):
    headers = {"Authorization": f"Bearer {TOKEN}", **kw.pop("headers", {})}
    r = requests.request(method, BASE + path, headers=headers, timeout=120, **kw)
    r.raise_for_status()
    return r


def transcribe(path, profile="oracle", language="auto"):
    name, size = os.path.basename(path), os.path.getsize(path)
    ctype = mimetypes.guess_type(name)[0] or "application/octet-stream"
    session = call("POST", "/api/v1/media/upload-session",
                   json={"filename": name, "file_size_bytes": size,
                         "content_type": ctype}).json()["data"]
    with open(path, "rb") as f:
        requests.put(session["upload_url"], data=f, headers=session["headers"],
                     timeout=3600).raise_for_status()
    media = call("POST", "/api/v1/media/complete",
                 json={"finalize_token": session["finalize_token"]}).json()["data"]
    job = call("POST", "/api/v1/jobs",
               headers={"Idempotency-Key": f"{name}-{size}-{profile}-{language}"},
               json={"type": "transcription",
                     "input": {"type": "media_ref", "media_id": media["media_id"]},
                     "profile": profile,
                     "options": {"language": language,
                                 "diarization": {"mode": "regular"}}}).json()["data"]
    while True:
        status = call("GET", f"/api/v1/jobs/{job['job_id']}").json()["data"]["status"]
        if status in ("succeeded", "failed", "cancelled"):
            break
        time.sleep(5)
    if status != "succeeded":
        raise RuntimeError(f"job {job['job_id']} {status}")
    export = call("GET", f"/api/v1/jobs/{job['job_id']}/export",
                  params={"format": "json"}).json()
    return job["job_id"], json.loads(export["data"])  # data is a JSON string


job_id, transcript = transcribe("interview.wav")
for seg in transcript["segments"]:
    print(f"{seg['start']:.2f}-{seg['end']:.2f} {seg.get('speaker', '')} {seg['text']}")
```

Large files are uploaded in parts: the session then returns `data.upload_url`
as `null` and a URL for each part in `data.multipart_part_urls` (see the
[quickstart](/quickstart.md)).

## 4. Use the timings

The JSON export has `segments`, each with `start` and `end` in seconds, `text`,
a `speaker` when diarization ran, and `words` (`text`, `start`, `end`). Let the
agent choose segments by index and read the times from the transcript; never let
a model type timestamps.

```python
import subprocess

keep = [s for s in transcript["segments"] if "pricing" in s["text"].lower()]
for i, seg in enumerate(keep):
    subprocess.run(["ffmpeg", "-y", "-i", "talk.mp4", "-ss", str(seg["start"]),
                    "-to", str(seg["end"]), f"clip{i}.mp4"], check=True)
```

Subtitles come straight from the API. `include_speakers=false` leaves the speaker
names out, and `max_chars_per_entry` sets the length of each subtitle entry:

```bash
curl -H "Authorization: Bearer $KOLWRITE_TOKEN" -o talk.srt \
  "https://api.kolwrite.com/api/v1/jobs/JOB_ID/export?format=srt&include_speakers=false&max_chars_per_entry=42"
```

SRT and VTT downloads need a Plus plan or higher. Other plans get HTTP 403 with
the code `subtitle_export_requires_plus`; JSON and text exports are not affected.

## 5. Rules for agents

- Send an `Idempotency-Key` when creating a job, so a retry never starts a second one.
- Retry `429`, `502`, `503` and `504` with exponential backoff; on `429` wait for
  `Retry-After`. Do not retry `401`, `403` or validation errors. See [errors](/errors.md).
- Read `detail.code`, not the message text.
- A job reserves credits when it is created and settles when it finishes; check
  `GET /api/v1/billing/balance` before a large batch.
- The OpenAPI file is the contract: every operation lists its required scope in
  `x-required-scope`.

## Questions

### How do I make SRT subtitles from a video with an AI agent?

Upload the file, create a transcription job, wait for `succeeded`, then call the
export endpoint with `format=srt`. Subtitles need a Plus plan or higher.

### Can Claude Code or Codex call the KolWrite API?

Yes. They call it over HTTPS with a personal access token from an environment
variable. There is no SDK or MCP server to install.

### How does an agent get word timestamps?

Export the job as JSON. Each segment carries `words` with `start` and `end` in
seconds.
