# Transcriptions

Turn an audio file into text. The endpoint is compatible with OpenAI Audio Transcriptions, so the official OpenAI SDKs work with the base URL `https://kenari.id/v1`. Only speech-to-text models are served here. Sending any other model returns `400`.

`POST /v1/audio/transcriptions`

Find the speech-to-text models with:

```bash
curl -s https://kenari.id/v1/models \
  | jq '.data[] | select(.endpoints | index("audio_transcription")) | {id, pricing_lines}'
```

## Request

The request is `multipart/form-data`.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `model` | string | yes | Speech-to-text model id, for example `whisper-large-v3-turbo`. |
| `file` | file | yes | The audio to transcribe. |
| `language` | string | no | Language code of the audio, for example `id` or `en`. Detected automatically when left out. |
| `prompt` | string | no | Text that guides spelling of names and terms or the style of the transcript. |
| `temperature` | number | no | Sampling temperature. Passed to the provider unchanged. |
| `response_format` | string | no | `json` (default), `verbose_json`, `text`, `srt` or `vtt`. |
| `timestamp_granularities[]` | string | no | `word` or `segment`. Repeat the field to ask for both. Applied when the model supports it. |

kenari does not check the audio format. Which formats work depends on the model. The whole request, file included, can be up to 32 MiB, and a larger body is rejected with `413`. A provider has 120 seconds to answer, so split very long recordings into several files.

Other fields are ignored.

## Response

With `json`, or when `response_format` is left out, the response holds only the text:

```json
{ "text": "Hello, this is an audio transcription." }
```

With `verbose_json`, the response adds `task`, `language`, `duration` in seconds, and `segments` with timing for each segment. Fields the model does not report are left out.

```json
{
  "text": "Hello, this is an audio transcription.",
  "task": "transcribe",
  "language": "en",
  "duration": 8.5,
  "segments": [
    {
      "id": 0,
      "seek": 0,
      "start": 0.0,
      "end": 3.2,
      "text": "Hello, this is an audio transcription.",
      "tokens": [50364, 1234],
      "temperature": 0.0,
      "avg_logprob": -0.28,
      "compression_ratio": 1.23,
      "no_speech_prob": 0.008
    }
  ]
}
```

kenari rebuilds the response from the fields shown above, so any other field the model returns is not passed through. A `words` array appears only if the model returns one, for example after `timestamp_granularities[]=word` on a model that supports it.

With `text`, the response body is the plain transcript, not JSON.

With `srt` or `vtt`, the response body is a subtitle file with one cue per segment. `srt` arrives as `text/plain` and `vtt` as `text/vtt`. When the model reports no segments, the file holds a single cue with the whole transcript.

```text
1
00:00:00,000 --> 00:00:03,200
Hello, this is an audio transcription.
```

## Examples

The examples use `recording.mp3`. Replace it with the path to your own recording.

### curl

```bash
curl https://kenari.id/v1/audio/transcriptions \
  -H "Authorization: Bearer $KENARI_API_KEY" \
  -F "file=@recording.mp3" \
  -F "model=whisper-large-v3-turbo" \
  -F "language=en"
```

### Python

```python
import os

from openai import OpenAI

client = OpenAI(
    base_url="https://kenari.id/v1",
    api_key=os.environ["KENARI_API_KEY"],
)

with open("recording.mp3", "rb") as audio:
    transcript = client.audio.transcriptions.create(
        model="whisper-large-v3-turbo",
        file=audio,
        language="en",
    )

print(transcript.text)
```

### JavaScript

```js
import fs from "node:fs";
import OpenAI from "openai";

const client = new OpenAI({
  baseURL: "https://kenari.id/v1",
  apiKey: process.env.KENARI_API_KEY,
});

const transcript = await client.audio.transcriptions.create({
  model: "whisper-large-v3-turbo",
  file: fs.createReadStream("recording.mp3"),
  language: "en",
});

console.log(transcript.text);
```

## Billing

Transcription is billed per second of audio, rounded up. The reservation is based on the duration kenari estimates from the file size, so a request can fail with `402` even when the audio is short. See [How billing works](/en/docs/billing) for how the reservation and the charge work, and the model's price in [Models and pricing](/en/docs/models).

## Errors

| Status | Code | When |
| --- | --- | --- |
| 400 | `bad_request` | The model is not a speech-to-text model, `file` or `model` is missing, the multipart body is malformed or cut off, or the provider rejected the audio. |
| 413 | none | The request body is above 32 MiB. The response is plain text. |

See [Errors](/en/docs/errors) for the shared codes, such as `insufficient_balance` and `all_providers_failed`.

## See also

- [Text to speech](/en/docs/audio-speech)
- [Models and pricing](/en/docs/models)
- [How billing works](/en/docs/billing)
- [Errors](/en/docs/errors)
