Audio Transcriptions
curl --request POST \
--url https://kymaapi.com/v1/audio/transcriptions \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"model": "<string>",
"language": "<string>",
"response_format": "<string>",
"temperature": 123,
"prompt": "<string>",
"audio_url": "<string>"
}
'import requests
url = "https://kymaapi.com/v1/audio/transcriptions"
payload = {
"model": "<string>",
"language": "<string>",
"response_format": "<string>",
"temperature": 123,
"prompt": "<string>",
"audio_url": "<string>"
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
model: '<string>',
language: '<string>',
response_format: '<string>',
temperature: 123,
prompt: '<string>',
audio_url: '<string>'
})
};
fetch('https://kymaapi.com/v1/audio/transcriptions', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://kymaapi.com/v1/audio/transcriptions",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'model' => '<string>',
'language' => '<string>',
'response_format' => '<string>',
'temperature' => 123,
'prompt' => '<string>',
'audio_url' => '<string>'
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://kymaapi.com/v1/audio/transcriptions"
payload := strings.NewReader("{\n \"model\": \"<string>\",\n \"language\": \"<string>\",\n \"response_format\": \"<string>\",\n \"temperature\": 123,\n \"prompt\": \"<string>\",\n \"audio_url\": \"<string>\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://kymaapi.com/v1/audio/transcriptions")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"model\": \"<string>\",\n \"language\": \"<string>\",\n \"response_format\": \"<string>\",\n \"temperature\": 123,\n \"prompt\": \"<string>\",\n \"audio_url\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://kymaapi.com/v1/audio/transcriptions")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"model\": \"<string>\",\n \"language\": \"<string>\",\n \"response_format\": \"<string>\",\n \"temperature\": 123,\n \"prompt\": \"<string>\",\n \"audio_url\": \"<string>\"\n}"
response = http.request(request)
puts response.read_body{
"text": "<string>",
"language": "<string>",
"duration": 123,
"segments": [
{}
],
"model": "<string>",
"billing.billable_minutes": 123,
"billing.cost_usd": 123,
"billing.balance_usd": 123
}Endpoints
Audio Transcriptions
Speech-to-text. Two input modes — multipart file (25 MB, OpenAI Whisper compatible) or JSON audio_url (Kyma extension; 100 MB on whisper-v3-turbo, 25 MB on other models). Synchronous, billed per minute.
POST
/
v1
/
audio
/
transcriptions
Audio Transcriptions
curl --request POST \
--url https://kymaapi.com/v1/audio/transcriptions \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"model": "<string>",
"language": "<string>",
"response_format": "<string>",
"temperature": 123,
"prompt": "<string>",
"audio_url": "<string>"
}
'import requests
url = "https://kymaapi.com/v1/audio/transcriptions"
payload = {
"model": "<string>",
"language": "<string>",
"response_format": "<string>",
"temperature": 123,
"prompt": "<string>",
"audio_url": "<string>"
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
model: '<string>',
language: '<string>',
response_format: '<string>',
temperature: 123,
prompt: '<string>',
audio_url: '<string>'
})
};
fetch('https://kymaapi.com/v1/audio/transcriptions', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://kymaapi.com/v1/audio/transcriptions",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'model' => '<string>',
'language' => '<string>',
'response_format' => '<string>',
'temperature' => 123,
'prompt' => '<string>',
'audio_url' => '<string>'
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://kymaapi.com/v1/audio/transcriptions"
payload := strings.NewReader("{\n \"model\": \"<string>\",\n \"language\": \"<string>\",\n \"response_format\": \"<string>\",\n \"temperature\": 123,\n \"prompt\": \"<string>\",\n \"audio_url\": \"<string>\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://kymaapi.com/v1/audio/transcriptions")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"model\": \"<string>\",\n \"language\": \"<string>\",\n \"response_format\": \"<string>\",\n \"temperature\": 123,\n \"prompt\": \"<string>\",\n \"audio_url\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://kymaapi.com/v1/audio/transcriptions")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"model\": \"<string>\",\n \"language\": \"<string>\",\n \"response_format\": \"<string>\",\n \"temperature\": 123,\n \"prompt\": \"<string>\",\n \"audio_url\": \"<string>\"\n}"
response = http.request(request)
puts response.read_body{
"text": "<string>",
"language": "<string>",
"duration": 123,
"segments": [
{}
],
"model": "<string>",
"billing.billable_minutes": 123,
"billing.cost_usd": 123,
"billing.balance_usd": 123
}Synchronous endpoint. Send audio in, get the transcript back in one call. Two input modes:
Mode 2 — JSON
If you need timestamps or confidence per segment, use
Billed per minute, rounded up (a 5-second clip costs 1 minute). Failover transcriptions are billed at the same rate as the primary
A transcription that ran is charged for the duration the provider measured, even if your balance fell below it while the file was being transcribed — the same rule
Returns the bare transcript. If the primary model has a transient hiccup, the request transparently completes via a secondary or tertiary tier and you’ll see
- Mode 1 — File upload (
multipart/form-data, up to 25 MB). Drop-in OpenAI Whisper replacement. - Mode 2 — URL fetch (
application/json,audio_urlup to 100 MB onwhisper-v3-turboand 25 MB on every other model, https only). Kyma extension — pass a public URL, Kyma fetches the bytes upstream so you don’t proxy them through your client.
# Mode 1 — multipart upload (OpenAI-compatible)
curl -X POST https://kymaapi.com/v1/audio/transcriptions \
-H "Authorization: Bearer $KYMA_API_KEY" \
-F "file=@meeting.mp3" \
-F "model=transcribe"
# Mode 2 — JSON audio_url (Kyma extension)
curl -X POST https://kymaapi.com/v1/audio/transcriptions \
-H "Authorization: Bearer $KYMA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"audio_url": "https://cdn.example.com/podcast.mp3", "model": "transcribe"}'
Choosing a mode
| Mode 1 — multipart | Mode 2 — JSON audio_url | |
|---|---|---|
| Content-Type | multipart/form-data | application/json |
| Source field | file (binary part) | audio_url (https string) |
| Max size | 25 MB | 100 MB on whisper-v3-turbo; 25 MB on other models |
| OpenAI SDK compatible | Yes | No — use raw HTTP |
| Never-die failover | Full chain (timestamp + plain-text tiers) | Retry + timestamp-preserving secondary |
| Best for | Local files, recordings, short clips | Cloud-hosted media, podcasts, long-form |
Mode 2 is a Kyma extension. It is not part of the OpenAI Whisper API. The OpenAI Python and Node SDKs only support multipart upload — for
audio_url mode, use requests (Python), fetch (Node), or curl.Request
Both modes accept the same set of model / language / format parameters.Mode 1 — multipart upload
file
required
Audio file. Accepted formats:
mp3, wav, m4a, ogg, flac, webm — the set every transcription model accepts, measured against each one. ogg and webm mean Opus in those containers, which is what browser MediaRecorder emits. Max 25 MB. ~30 minutes of mono 16kHz mp3 fits comfortably.The part’s Content-Type may be generic (application/octet-stream, or missing) — the file extension decides. Anything else is refused with 415 unsupported_audio_type before any charge.string
default:"transcribe"
Choose by alias or SKU:
transcribe(default) →whisper-v3-turbo— fastest and cheapest, $0.0009/min. Best for high-volume bulk transcription.transcribe-quality→gpt-4o-mini-transcribe-2025-12-15— premium accuracy on noisy / conversational / code-switching audio (Vi/En mixing), $0.00405/min. Retiring 2027-02-26 — seegpt-transcribebelow.gpt-transcribe— OpenAI’s current premium STT and the replacement it names for the model above, $0.006075/min. Opt-in by SKU; thetranscribe-qualityalias is unchanged.gemini-3.5-transcribe— Google file STT, $0.00675/min. Opt-in by SKU; thetranscribealias is unchanged.
verbose_json, srt or vtt.- Or pin a specific SKU directly (e.g.
whisper-v3-turbo,gpt-transcribe,gpt-4o-mini-transcribe-2025-12-15,gemini-3.5-transcribe). See Audio models and Model aliases.
string
ISO-639-1 code (e.g.
en, vi, ja). Optional — both providers auto-detect when omitted. Supplying it improves accuracy on short clips.string
One of:
json, verbose_json, text, srt, vtt. JSON formats embed a billing block in the response body. text returns the bare transcript and srt / vtt return subtitle files; for those three, billing rides on X-Kyma-* response headers so the body stays a clean transcript or subtitle file.verbose_json, srt and vtt need per-segment timestamps. On a model that returns a plain transcript they are refused with 400 segments_unsupported — before any provider call, so nothing is charged. See Segment timestamps by model.number
default:"0"
Sampling temperature 0–1. Default 0 (deterministic).
string
Optional priming text. Use it to nudge the model toward known proper nouns, acronyms, or domain vocabulary in your audio.
Mode 2 — JSON audio_url
string
required
Public HTTPS URL of the audio file. Kyma fetches the bytes upstream — no need to download and re-upload from your client. Max 100 MB on
whisper-v3-turbo; every other model, and the whisper-1 failover, fetches at most 25 MB and fails a larger file. http:// and other schemes are rejected to prevent SSRF / mixed-content. The bytes are fetched and passed to the model as the host serves them — Kyma does not inspect the URL’s Content-Type — so use one of the Mode 1 formats; anything else is the model’s own error.string
default:"transcribe"
Same as Mode 1.
string
Same as Mode 1.
string
Same as Mode 1. Mode 2 still benefits from never-die failover (retry + the timestamp-preserving secondary); the plain-text tertiary tier is skipped in URL mode. See Automatic failover.
number
Same as Mode 1.
string
Same as Mode 1.
Response
200 OK with the transcript and a Kyma billing block.
{
"task": "transcribe",
"language": "English",
"duration": 5.03,
"text": "For too long, I have watched mortals suffer.",
"segments": [
{
"id": 0,
"start": 0,
"end": 4.74,
"text": "For too long, I have watched mortals suffer.",
"tokens": [50365, 1171, 886, 938, 11, 286, 362, 6337, 6599, 1124, 9753, 13, 50602],
"temperature": 0,
"avg_logprob": -0.20,
"compression_ratio": 0.85,
"no_speech_prob": 0.0
}
],
"model": "whisper-v3-turbo",
"billing": {
"duration_sec": 5.03,
"billable_minutes": 1,
"cost_usd": 0.0009,
"balance_usd": 41.469
}
}
string
The full transcript.
string
Detected language (full name, e.g.
"English"). Like task, duration and segments, it is returned only with verbose_json, which only timestamp models serve. With json the body is text, model and billing, plus logprobs when you asked for them.number
Audio duration in seconds (decoded from the file, not estimated).
0 only when the plain-text tertiary tier (gemini-3-flash-audio) served the request, which does not return duration.array
Per-segment timestamps and text. Present when
response_format is verbose_json and the model returns timestamps (supports_segments: true on /v1/models) — whisper-v3-turbo, or its failover whisper-1. A model without them cannot be asked for verbose_json at all (400 segments_unsupported).string
The model SKU you requested, which is the one you are billed for. It stays the same when a failover model served; see
X-Kyma-Fallback.number
Minutes charged. Audio is billed in 1-minute increments, rounded up.
number
Final cost charged for this request.
number
Remaining balance after this charge.
Non-JSON formats
Whenresponse_format is text, srt, or vtt, the body is a plain transcript or subtitle file (no JSON envelope) and billing comes back on response headers (see below).
srt returns a SubRip subtitle file (application/x-subrip; charset=utf-8); vtt returns a WebVTT file (text/vtt; charset=utf-8). Both are built from the same per-segment timestamps verbose_json exposes, so the timing matches across formats.
Segment timestamps by model
Only some transcription models return per-segment timestamps (segments[] with start, end, avg_logprob, no_speech_prob). The catalogue says which: GET /v1/models carries supports_segments on every transcription SKU, and a request for verbose_json / srt / vtt on a model without them is refused up front with 400 segments_unsupported and a suggested_models list.
| Model | Returns segments | Formats available |
|---|---|---|
whisper-v3-turbo | Yes | json, verbose_json, text, srt, vtt |
gpt-4o-mini-transcribe-2025-12-15 | No | json, text |
gpt-transcribe | No | json, text |
gemini-3.5-transcribe | No | json, text |
whisper-v3-turbo. Cutting a plain transcript into pseudo-segments client-side and estimating times from character counts produces invented timings — it is not something Kyma does for you.
Response headers
Returned ontext, srt and vtt responses. json and verbose_json responses carry the same figures in the body’s billing block instead, and set none of these headers.
| Header | Meaning |
|---|---|
X-Kyma-Model | The model SKU you requested and are billed for (e.g. whisper-v3-turbo) |
X-Kyma-Duration-Sec | Detected audio duration in seconds (0 only when the plain-text tertiary tier served) |
X-Kyma-Billable-Minutes | Minutes charged |
X-Kyma-Cost-USD | Final cost in USD |
X-Kyma-Balance-USD | Remaining account balance |
X-Kyma-Fallback | Secondary model SKU that served (e.g. whisper-1, gemini-3-flash-audio) only when a secondary served. Absent on the primary path and on a bare primary retry. |
Automatic failover (never-die STT)
Transcription is never-die. On a transient hiccup with the primary model (defaulttranscribe alias / whisper-v3-turbo SKU), Kyma transparently works through a failover chain so your request still completes — same request, same price, same response shape. You don’t opt in: it’s on by default.
Why it matters. A single transcription request is normally a single point of failure — one transient hiccup and the pipeline built on top of it (a dub, a caption job, a voice agent) dies, leaving you to write retry and fallback logic yourself. Kyma absorbs that for you: the request either completes or fails honestly — never silently mangled. You ship the feature, not the plumbing.
The chain is format-aware, because only some models preserve per-segment timestamps:
- Retry the primary once (short backoff). Absorbs the vast majority of transient blips, which recover immediately. Nothing in your response changes.
- Secondary —
whisper-1. A timestamp-preserving model. Because it returns real per-segment timestamps, it serves every caller, includingverbose_json,srt, andvtt. - Tertiary —
gemini-3-flash-audio. A plain-text transcription tier. Used only for plain-text transcripts (text/json) — it does not return segment timestamps, so callers that need them (verbose_json/srt/vtt) never route here. URL mode (audio_url) also skips this tier (it needs inline bytes).
400 / 413 / 415 / 422) is surfaced immediately with no failover — failover is a reliability tool, not an error-hiding one. On transcribe, a rate limit on the primary model moves the request down the chain like any other hiccup. On the models that do not fail over, it returns 429 with code provider_rate_limited and a Retry-After header.
You can see which tier served:
X-Kyma-Fallbackheader — the secondary model SKU that served, when a secondary served. It is set ontext,srtandvttresponses only; ajsonorverbose_jsonresponse does not show that a failover happened.segmentsis present whenever a timestamp-preserving model served (primary orwhisper-1); absent only on the plain-text tertiary tier, wheredurationis also0(billed as the 1-minute minimum).
Only
whisper-v3-turbo (the transcribe alias) has the chain. The other SKUs opt out — you chose that model, so Kyma won’t silently swap it. That covers the transcribe-quality alias (gpt-4o-mini-transcribe-2025-12-15), gpt-transcribe and gemini-3.5-transcribe. Their errors surface as 502 transcription_failed.Pricing
| Alias | SKU | Per minute | 1-hour file |
|---|---|---|---|
transcribe (default) | whisper-v3-turbo | $0.0009 | $0.054 |
transcribe-quality | gpt-4o-mini-transcribe-2025-12-15 | $0.00405 | $0.243 |
| — | gpt-transcribe | $0.006075 | $0.3645 |
| — | gemini-3.5-transcribe | $0.00675 | $0.405 |
transcribe path — it doesn’t matter which tier ultimately served, you always pay the whisper-v3-turbo price. The Quality tier does not fall back (your choice of model is respected).
Errors
| Status | error.type | error.code | When |
|---|---|---|---|
400 | invalid_request | — | Missing file (Mode 1) or invalid JSON body (Mode 2) |
400 | invalid_request | audio_url_required | Mode 2 body without audio_url field |
400 | invalid_request | audio_url_invalid_scheme | audio_url not https:// (SSRF protection) |
400 | invalid_request | not_a_transcription_model | model is not a transcription SKU. error.suggested_models lists ones that are. |
400 | invalid_request | segments_unsupported | verbose_json / srt / vtt requested on a model that returns no per-segment timestamps. Refused before any provider call — nothing is charged. error.suggested_models lists timestamp-preserving models. |
401 | auth_error | — | Missing or invalid API key |
402 | billing_error | insufficient_credits | Balance does not cover the file. Checked before the audio leaves Kyma: the amount is the file’s own length — read exactly from an uncompressed file’s header, estimated conservatively otherwise — with one minute as the floor. Nothing is charged, and no provider is called. |
408 | timeout | audio_upload_timeout | The multipart upload did not finish arriving in time, usually a slow connection. Retry, or use Mode 2 |
413 | invalid_request | — | Multipart body > 25 MB. For larger files, use Mode 2 with whisper-v3-turbo (audio_url up to 100 MB). |
415 | invalid_request | unsupported_audio_type | The part is not one of the accepted formats. A generic application/octet-stream is fine when the filename carries the extension. Not charged. |
422 | invalid_request | no_speech_detected | The file decoded but contains no speech (silent, or damaged in a convert step). Not charged. Listen to the file that was actually sent, not the source recording. error.suggested_models is included. |
429 | rate_limit_error | audio_concurrent_limit | Your tier’s audio concurrency cap is reached. Retry-After: 5. See Rate Limits. |
429 | rate_limit_error | provider_rate_limited | The model is rate-limited and the SKU does not fail over (or the chain was exhausted). Wait for Retry-After. |
502 | provider_error | transcription_failed | The never-die chain was exhausted (every eligible tier failed), or a premium SKU that does not fail over is down. The error message is provider-isolated — no upstream identity is leaked. error.suggested_models names transcription models you can switch to in the meantime. |
503 | provider_error | — | Transcription is not available right now. Retry shortly. |
/v1/chat/completions follows. The balance can go slightly negative for that one request; the next request is refused with 402 until you top up.
Every transcription error that leaves you without a transcript carries error.suggested_models: up to three other transcription model ids, filtered to ones that can serve the format you asked for. They are suggestions — Kyma never swaps the model you named.
Examples
Pin a specific model
curl -X POST https://kymaapi.com/v1/audio/transcriptions \
-H "Authorization: Bearer $KYMA_API_KEY" \
-F "file=@interview.mp3" \
-F "model=whisper-v3-turbo" \
-F "response_format=verbose_json" \
-F "language=en"
Just the transcript text (full never-die chain)
curl -X POST https://kymaapi.com/v1/audio/transcriptions \
-H "Authorization: Bearer $KYMA_API_KEY" \
-F "file=@clip.mp3" \
-F "model=transcribe" \
-F "response_format=text"
X-Kyma-Fallback naming the model SKU that served.
URL mode — Node fetch
const response = await fetch("https://kymaapi.com/v1/audio/transcriptions", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.KYMA_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
audio_url: "https://cdn.example.com/podcast.mp3",
model: "transcribe",
response_format: "verbose_json",
}),
});
const result = await response.json();
console.log(result.text);
URL mode — Python requests
import os
import requests
resp = requests.post(
"https://kymaapi.com/v1/audio/transcriptions",
headers={"Authorization": f"Bearer {os.environ['KYMA_API_KEY']}"},
json={
"audio_url": "https://cdn.example.com/podcast.mp3",
"model": "transcribe",
"response_format": "verbose_json",
},
)
result = resp.json()
print(result["text"])
The OpenAI Python SDK only supports multipart uploads — use
requests directly for Mode 2.Python (OpenAI SDK — Mode 1 only)
from openai import OpenAI
client = OpenAI(
base_url="https://kymaapi.com/v1",
api_key="kyma-...",
)
with open("meeting.mp3", "rb") as f:
result = client.audio.transcriptions.create(
model="transcribe",
file=f,
)
print(result.text)
See also
- Audio Understand — the rest of the audio scene (tone, music, mood)
- Audio models — SKUs behind the
transcribealias - Rate Limits — concurrency caps for audio endpoints
watch-cli— open-source CLI that uses these endpoints to give any agent eyes and ears for any social video