## Transcribe audio

`POST /v1/audio/transcriptions`

Converts an audio file to text via Whisper Large v3 Turbo — no BYOK required. The transcription counts toward the Bitrix24 account's AI quota by audio duration: within the plan's quota nothing is charged to the balance, above the quota the usage is charged to the account's money balance at the model's base price. Accepts the file via `multipart/form-data`. The request and response format is compatible with `POST /v1/audio/transcriptions` from the OpenAI API.

> **The response comes in raw OpenAI format.**
>
> There is no `{success, data}` wrapper used by the other Vibecode endpoints — `/v1/deals`, `/v1/tasks` and others.
>
> This is done for compatibility with the OpenAI SDK. If you have a single client that checks `if (!response.success)`, add an exception for the AI Router.

## Request fields (form-data)

| Field | Type | Req. | Default | Description |
|------|-----|:-----:|-----------|----------|
| `file` | file | yes | — | Audio file. Supported: `mp3`, `mp4`, `mpeg`, `mpga`, `m4a`, `wav`, `webm`, `flac`, `ogg`. Maximum size — 25 MB |
| `model` | string | no | `deepdml/faster-whisper-large-v3-turbo-ct2` | Whisper model ID. The `bitrix/` prefix is optional and is stripped automatically |
| `language` | string | no | auto-detection | Language code per `ISO 639` (2-3 letters): `ru`, `en`, `de`, `fr`, `zh`, etc. Specifying the language speeds up recognition |
| `prompt` | string | no | — | Context hint: the conversation topic, style, correct spelling of terms. Up to 2000 characters, the model uses the last ~224 tokens |
| `hotwords` | string | no | — | Special words and terms separated by commas — improve recognition accuracy for rare names and brands. Up to 500 characters |
| `temperature` | number | no | `0` | Decoder temperature from `0` to `1`. `0` — deterministic result, higher — more variability. Out-of-range values are rejected |
| `vad_filter` | boolean | no | — | `true` enables the VAD filter: the model cuts out silence before recognition — fewer hallucinations on recordings with pauses |
| `timestamp_granularities[]` | string | no | `segment` | Timestamp granularity: `word` or `segment` (the field can be repeated). Only with `response_format=verbose_json`. With `word`, each segment is augmented with a `words` array carrying the timing and probability of each word |
| `response_format` | string | no | `json` | Result format: `json`, `text`, `srt`, `vtt`, `verbose_json` |

## Examples

### curl — personal key

```bash
curl -X POST https://vibecode.bitrix24.com/v1/audio/transcriptions \
  -H "X-Api-Key: YOUR_API_KEY" \
  -F "file=@call-recording.mp3" \
  -F "language=en" \
  -F "response_format=json"
```

### curl — OAuth application

```bash
curl -X POST https://vibecode.bitrix24.com/v1/audio/transcriptions \
  -H "X-Api-Key: YOUR_APP_KEY" \
  -H "Authorization: Bearer USER_SESSION_TOKEN" \
  -F "file=@call-recording.mp3" \
  -F "language=en" \
  -F "response_format=json"
```

### JavaScript — personal key

```javascript
const formData = new FormData()
formData.append('file', audioFile)  // File or Blob object
formData.append('language', 'en')
formData.append('response_format', 'json')

const res = await fetch('https://vibecode.bitrix24.com/v1/audio/transcriptions', {
  method: 'POST',
  headers: { 'X-Api-Key': 'YOUR_API_KEY' },
  body: formData,
})

const result = await res.json()
console.log('Recognized text:', result.text)
```

### JavaScript — OAuth application

```javascript
const formData = new FormData()
formData.append('file', audioFile)
formData.append('language', 'en')

const res = await fetch('https://vibecode.bitrix24.com/v1/audio/transcriptions', {
  method: 'POST',
  headers: {
    'X-Api-Key': 'YOUR_APP_KEY',
    'Authorization': 'Bearer USER_SESSION_TOKEN',
  },
  body: formData,
})

const result = await res.json()
console.log('Text:', result.text)
```

## Response fields

The response structure depends on `response_format`. The `json` format (default) is the most compact, `verbose_json` — with timings.

### `response_format: json`

| Field | Type | Description |
|------|-----|----------|
| `text` | string | The full recognized text |

### `response_format: text`

The response is a plain string with the recognized text, without a `JSON` wrapper.

### `response_format: verbose_json`

| Field | Type | Description |
|------|-----|----------|
| `task` | string | Task type: `transcribe` |
| `language` | string | Code of the detected or specified language |
| `duration` | number | Audio duration in seconds |
| `text` | string | The full recognized text |
| `usage` | object \| null | Model usage metadata. `null` if the model does not return it |
| `words` | array | Word-level timings at the whole-response level. Empty array if word-level timings were not requested |
| `segments` | array | Segments with timings and recognition metadata |
| `segments[].id` | number | Segment ordinal number |
| `segments[].seek` | number | Whisper's internal recognition-window offset |
| `segments[].start` | number | Segment start in seconds |
| `segments[].end` | number | Segment end in seconds |
| `segments[].text` | string | Segment text |
| `segments[].tokens` | array | Model tokens for the segment text (integers) |
| `segments[].temperature` | number | Decoding temperature applied by the model |
| `segments[].avg_logprob` | number | Average log probability of the segment tokens — a confidence metric |
| `segments[].compression_ratio` | number | Compression ratio of the segment text |
| `segments[].no_speech_prob` | number | Probability that the segment contains no speech |
| `segments[].words` | array | Word-level timings inside the segment. Empty array if not requested |
| `segments[].emotion` | string \| null | Detected emotion of the segment. `null` if not detected |

### `response_format: srt` / `vtt`

The response is subtitles in `SubRip Text` or `WebVTT` format.

## Response example

`response_format: json`:

```json
{
  "text": "Hello, this is Acme LLC. We want a CRM for 50 users, budget up to 500 thousand per month."
}
```

`response_format: verbose_json`:

```json
{
  "task": "transcribe",
  "language": "en",
  "duration": 8.42,
  "text": "Hello, this is Acme LLC. We want a CRM for 50 users, budget up to 500 thousand per month.",
  "usage": null,
  "words": [],
  "segments": [
    {
      "id": 0,
      "seek": 0,
      "start": 0.0,
      "end": 3.2,
      "text": "Hello, this is Acme LLC.",
      "tokens": [50365, 2425, 11, 341, 307],
      "temperature": 0.0,
      "avg_logprob": -0.38,
      "compression_ratio": 1.12,
      "no_speech_prob": 0.02,
      "words": [],
      "emotion": null
    },
    {
      "id": 1,
      "seek": 320,
      "start": 3.2,
      "end": 8.42,
      "text": "We want a CRM for 50 users, budget up to 500 thousand per month.",
      "tokens": [50414, 1003, 13767, 295, 1500],
      "temperature": 0.0,
      "avg_logprob": -0.41,
      "compression_ratio": 1.20,
      "no_speech_prob": 0.01,
      "words": [],
      "emotion": null
    }
  ]
}
```

## Error response example

`400 no_file` — the `file` field was not provided:

```json
{
  "error": {
    "message": "Audio file is required. Send as multipart/form-data with field \"file\".",
    "type": "invalid_request_error",
    "code": "no_file"
  }
}
```

## Errors

| HTTP | Code | Description |
|------|-----|----------|
| 400 | `no_file` | The `file` field was not provided in `multipart/form-data` |
| 400 | `empty_file` | The `file` field was provided, but the file is empty (0 bytes). Common cause: `curl -F "file=path"` without the `@` prefix — curl sends the path string instead of the file contents |
| 400 | `invalid_prompt` | The `prompt` field is longer than 2000 characters |
| 400 | `invalid_hotwords` | The `hotwords` field is longer than 500 characters |
| 400 | `invalid_temperature` | The `temperature` field is not a number or is outside the 0..1 range |
| 400 | `invalid_vad_filter` | The `vad_filter` field is neither `true` nor `false` |
| 400 | `invalid_timestamp_granularities` | The value is not `word`/`segment`, or the response format is not `verbose_json` |
| 400 | `invalid_language` | The language code does not match the `ISO 639` format (2-3 letters) |
| 402 | `ai_credentials_not_configured` | The Bitrix provider is not configured on the platform |
| 402 | `insufficient_balance` | The PREPAY account is past its overdraft — checked BEFORE the recognition call (PLATFORM/PORTAL keys only, BYOK is free and not checked). A fully frozen account is rejected earlier with `ACCOUNT_FROZEN` |
| 402 | `ai_quota_exhausted` | The portal's monthly AI quota is exhausted — see "Known specifics" below |
| 403 | `scope_missing` | The API key is missing the `vibe:ai` scope |
| 502 | `ai_provider_unavailable` | The Whisper service returned an error or is unavailable |
| 503 | `ai_provider_timeout` | Whisper did not respond within 15 minutes — the file is too long or the service is overloaded. For recordings longer than ~30 minutes, split them into parts |
| 401 | `MISSING_API_KEY` | The `X-Api-Key` header was not provided |
| 429 | `ai_congested` | The AI cluster pool is overloaded. The request was not executed, nothing was charged, retry it per the `Retry-After` header. The response carries the `X-AI-Admission: shed` header, not `X-RateLimit-Scope` |
| 429 | `ai_pacing_limited` | A daily or weekly pacing window is exceeded. This is not quota exhaustion — repeat the request after the time in the `Retry-After` header. See [Pacing (day/week smoothing)](/docs/ai/consumption/quota#pacing-day-week-smoothing) |

The full list of common API errors — [Errors](/docs/errors).

## Known specifics

**Hints improve accuracy on rare terms.** The `prompt` field sets the context — the conversation topic, style, correct spelling of terms. The `hotwords` field lists comma-separated special words the decoder gives higher priority to. The difference on the phrase "discussing the NeuralDeep integration with Kimi and the RAG approach":

| Request | Result |
|---|---|
| Without hints | "…the neural-dip integration with Key Me and the rug approach for search…" |
| With `hotwords` | "…the NeuralDeep integration with Kimi and the RAG approach for search…" |


**Within the quota — no charges, above the quota — the model's base price.** Whisper runs on Bitrix24 infrastructure. Transcription is priced by audio duration and counts toward the Bitrix24 account's monthly AI quota. As long as usage stays within the plan's quota, nothing is charged to the account balance. Above the quota, usage is charged to the account's money balance at the model's base catalog price. On accounts with quota control enabled, a request may return `402 ai_quota_exhausted` when the monthly limit is exhausted. The `reason` field distinguishes three cases: `breaker` — the hourly over-quota spending breaker fired, `wallet_empty` — the quota is exhausted and the account balance has no funds, `wallet_off` — over-quota spend is not available for the account. The `resetAt` field is the moment when requests will start passing again; for `wallet_off` it may be absent. In the `wallet_empty` case the response may additionally carry a `hint` string and a `topupUrl` link to top up the balance — both fields appear when enforced quota control and the top-up hint are enabled on the platform, so read them as optional. The `hint` field in this response is a string.

**Size limit — 25 MB.** For long recordings, split the file into parts up to 25 MB and stitch the results on the client side. One hour of `mp3` at 128 kbit/s is about 60 MB — you will have to cut it.

**Request timeout — 15 minutes.** Transcription takes roughly 5-15% of the audio length. A 5-minute file (5-7 MB `mp3`) transcribes in 15-45 seconds. On a timeout, `503 ai_provider_timeout` is returned.

## See also

- [Speech recognition](/docs/ai/audio)
- [Create a chat completion](/docs/ai/chat/completions)
- [AI Router](/docs/ai)
