Text to speech
curl --request POST \
--url https://api.kova.ai/v1/tts \
--header 'Content-Type: application/json' \
--header 'x-api-key: <api-key>' \
--data '
{
"text": "<string>",
"voice": "<string>",
"normalize_text": false,
"response_format": {
"bitrate": "<string>",
"encoding": "mp3",
"sample_rate": 123
},
"temperature": 123,
"timestamps": false
}
'import requests
url = "https://api.kova.ai/v1/tts"
payload = {
"text": "<string>",
"voice": "<string>",
"normalize_text": False,
"response_format": {
"bitrate": "<string>",
"encoding": "mp3",
"sample_rate": 123
},
"temperature": 123,
"timestamps": False
}
headers = {
"x-api-key": "<api-key>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'x-api-key': '<api-key>', 'Content-Type': 'application/json'},
body: JSON.stringify({
text: '<string>',
voice: '<string>',
normalize_text: false,
response_format: {bitrate: '<string>', encoding: 'mp3', sample_rate: 123},
temperature: 123,
timestamps: false
})
};
fetch('https://api.kova.ai/v1/tts', 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://api.kova.ai/v1/tts",
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([
'text' => '<string>',
'voice' => '<string>',
'normalize_text' => false,
'response_format' => [
'bitrate' => '<string>',
'encoding' => 'mp3',
'sample_rate' => 123
],
'temperature' => 123,
'timestamps' => false
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json",
"x-api-key: <api-key>"
],
]);
$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://api.kova.ai/v1/tts"
payload := strings.NewReader("{\n \"text\": \"<string>\",\n \"voice\": \"<string>\",\n \"normalize_text\": false,\n \"response_format\": {\n \"bitrate\": \"<string>\",\n \"encoding\": \"mp3\",\n \"sample_rate\": 123\n },\n \"temperature\": 123,\n \"timestamps\": false\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("x-api-key", "<api-key>")
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://api.kova.ai/v1/tts")
.header("x-api-key", "<api-key>")
.header("Content-Type", "application/json")
.body("{\n \"text\": \"<string>\",\n \"voice\": \"<string>\",\n \"normalize_text\": false,\n \"response_format\": {\n \"bitrate\": \"<string>\",\n \"encoding\": \"mp3\",\n \"sample_rate\": 123\n },\n \"temperature\": 123,\n \"timestamps\": false\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.kova.ai/v1/tts")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["x-api-key"] = '<api-key>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"text\": \"<string>\",\n \"voice\": \"<string>\",\n \"normalize_text\": false,\n \"response_format\": {\n \"bitrate\": \"<string>\",\n \"encoding\": \"mp3\",\n \"sample_rate\": 123\n },\n \"temperature\": 123,\n \"timestamps\": false\n}"
response = http.request(request)
puts response.read_body{
"audio": "<string>",
"timestamps": {
"end_seconds": [
123
],
"start_seconds": [
123
],
"words": [
"<string>"
]
}
}{
"detail": [
{
"loc": [
"<string>"
],
"msg": "<string>",
"type": "<string>",
"ctx": {},
"input": "<unknown>"
}
]
}HTTP API
Text to speech
Synchronous TTS. Returns base64-encoded audio after generation completes.
POST
/
v1
/
tts
Text to speech
curl --request POST \
--url https://api.kova.ai/v1/tts \
--header 'Content-Type: application/json' \
--header 'x-api-key: <api-key>' \
--data '
{
"text": "<string>",
"voice": "<string>",
"normalize_text": false,
"response_format": {
"bitrate": "<string>",
"encoding": "mp3",
"sample_rate": 123
},
"temperature": 123,
"timestamps": false
}
'import requests
url = "https://api.kova.ai/v1/tts"
payload = {
"text": "<string>",
"voice": "<string>",
"normalize_text": False,
"response_format": {
"bitrate": "<string>",
"encoding": "mp3",
"sample_rate": 123
},
"temperature": 123,
"timestamps": False
}
headers = {
"x-api-key": "<api-key>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'x-api-key': '<api-key>', 'Content-Type': 'application/json'},
body: JSON.stringify({
text: '<string>',
voice: '<string>',
normalize_text: false,
response_format: {bitrate: '<string>', encoding: 'mp3', sample_rate: 123},
temperature: 123,
timestamps: false
})
};
fetch('https://api.kova.ai/v1/tts', 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://api.kova.ai/v1/tts",
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([
'text' => '<string>',
'voice' => '<string>',
'normalize_text' => false,
'response_format' => [
'bitrate' => '<string>',
'encoding' => 'mp3',
'sample_rate' => 123
],
'temperature' => 123,
'timestamps' => false
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json",
"x-api-key: <api-key>"
],
]);
$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://api.kova.ai/v1/tts"
payload := strings.NewReader("{\n \"text\": \"<string>\",\n \"voice\": \"<string>\",\n \"normalize_text\": false,\n \"response_format\": {\n \"bitrate\": \"<string>\",\n \"encoding\": \"mp3\",\n \"sample_rate\": 123\n },\n \"temperature\": 123,\n \"timestamps\": false\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("x-api-key", "<api-key>")
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://api.kova.ai/v1/tts")
.header("x-api-key", "<api-key>")
.header("Content-Type", "application/json")
.body("{\n \"text\": \"<string>\",\n \"voice\": \"<string>\",\n \"normalize_text\": false,\n \"response_format\": {\n \"bitrate\": \"<string>\",\n \"encoding\": \"mp3\",\n \"sample_rate\": 123\n },\n \"temperature\": 123,\n \"timestamps\": false\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.kova.ai/v1/tts")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["x-api-key"] = '<api-key>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"text\": \"<string>\",\n \"voice\": \"<string>\",\n \"normalize_text\": false,\n \"response_format\": {\n \"bitrate\": \"<string>\",\n \"encoding\": \"mp3\",\n \"sample_rate\": 123\n },\n \"temperature\": 123,\n \"timestamps\": false\n}"
response = http.request(request)
puts response.read_body{
"audio": "<string>",
"timestamps": {
"end_seconds": [
123
],
"start_seconds": [
123
],
"words": [
"<string>"
]
}
}{
"detail": [
{
"loc": [
"<string>"
],
"msg": "<string>",
"type": "<string>",
"ctx": {},
"input": "<unknown>"
}
]
}Building with an AI agent? Point it at
docs.kova.ai/for-agents.md — the complete API contract as one plain-text page.POST /v1/tts is the simplest way to generate audio: send text, get a complete audio file back as base64. Use it when you have a single utterance and want the finished file.
If you need playback to start before generation finishes, use Streaming TTS instead.
Generation runs at roughly 2× realtime, so this endpoint is slow on long input. Measured end-to-end: 500 characters ≈ 17 s, 1,000 ≈ 36 s, 2,000 ≈ 65 s, 5,000 ≈ 153 s. There is no server-side character limit, but a 10,000-character request exceeds a 180-second client timeout. Set generous timeouts, and prefer streaming above roughly 1,000 characters.
Request
{
"text": "Welcome to Kova.",
"voice": "cal",
"response_format": {"encoding": "mp3", "sample_rate": 32000, "bitrate": "128k"},
"timestamps": false,
"temperature": 1.0,
"normalize_text": false
}
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
text | string | yes | — | Empty and whitespace-only strings are accepted and return a short silent file. |
voice | string | yes | — | An id from GET /v1/tts/speakers. An unknown id returns 422 with the valid set in valid_voices. |
response_format | object | string | no | {"encoding":"mp3"} | A bare string like "mp3" is accepted as shorthand. null returns 422. |
timestamps | boolean | no | false | Adds word timings to the response. null returns 422. |
temperature | number | null | no | server default | Must be 0.5–3.0 inclusive. null means “use the default”. |
normalize_text | boolean | no | false | Expands numbers, dates and currency for speech. null returns 422. |
The schema is strict. Any field not in this table returns
422 with "Extra inputs are not permitted". There is no forward compatibility for unknown keys — check spelling carefully.Response
{
"audio": "<base64>",
"timestamps": {
"words": ["Welcome", "to", "Kova."],
"start_seconds": [0.06, 0.53, 0.9],
"end_seconds": [0.38, 0.76, 1.14]
}
}
audio is always present and always base64 — decode it before writing bytes to disk.
timestamps is present only when the request set timestamps: true; otherwise the key is absent or null. Its three arrays are parallel and equal in length: words[i] spans start_seconds[i] to end_seconds[i], measured in seconds from the start of the audio.
Word tokens keep their original punctuation — you’ll see
"Kova." and "$19.99.", not bare words. normalize_text changes how text is pronounced but does not change these tokens.Examples
from kova_tts import KovaTTSClient, AudioResponseFormat
client = KovaTTSClient(api_key="kova_sk_...")
result = client.tts(
text="Welcome to Kova.",
voice="cal",
response_format=AudioResponseFormat(encoding="mp3"),
timestamps=True,
)
client.write_audio_file(result.audio, "welcome.mp3")
if result.timestamps:
print(result.timestamps.words)
import { KovaTTSClient } from "@kova-ai/tts";
const client = new KovaTTSClient({ apiKey: "kova_sk_..." });
const result = await client.tts({
text: "Welcome to Kova.",
voice: "cal",
response_format: { encoding: "mp3" },
timestamps: true,
});
await client.writeAudioFile(result.audio, "welcome.mp3");
console.log(result.timestamps?.words);
curl https://api.kova.ai/v1/tts \
-H "x-api-key: $KOVA_API_KEY" \
-H "content-type: application/json" \
-d '{
"text": "Welcome to Kova.",
"voice": "cal",
"response_format": {"encoding": "mp3"},
"timestamps": true
}' \
| jq -r .audio | base64 -d > welcome.mp3
response_format
response_format accepts an object or, as shorthand, a bare encoding string (e.g. "mp3").
| encoding | sample_rate | default rate | bitrate | Output |
|---|---|---|---|---|
mp3 (default) | 16000–48000 Hz | 32000 | 32k–320k, default 128k | MPEG with an ID3 header. |
wav | 8000–48000 Hz | 32000 | not supported | RIFF header, one per file. |
pcm | 8000–48000 Hz | 32000 | not supported | Headerless signed 16-bit little-endian. |
linear16 | 8000–48000 Hz | 32000 | not supported | Same samples as pcm, wrapped in RIFF. |
opus | one of 8000, 12000, 16000, 24000, 48000 | 48000 | 32k–192k, default 64k | OggS container. |
mulaw | 8000 only | 8000 | not supported | G.711 µ-law, headerless, 1 byte/sample. |
alaw | 8000 only | 8000 | not supported | G.711 A-law, headerless, 1 byte/sample. |
bitrate is in kbps, never bps
"128k", "128" and 128 are all equivalent and all mean 128 kbps.
128000 is rejected with a 422. An integer bitrate is read as kilobits per second, so 128000 parses as 128,000 kbps — far out of range. Never pass bits per second.bitrate to wav, pcm, linear16, mulaw or alaw returns 422 with "<encoding> audio does not support a bitrate option". Omit the field for those encodings — sending null is fine.
Encoding names are case-insensitive and aliased
"MP3", "Mp3" and "mp3" are identical. Aliases are accepted and normalized:
| Alias | Resolves to |
|---|---|
linear_pcm, linear-pcm | linear16 (RIFF header) |
pcm_s16le, raw | pcm (headerless) |
mu-law, ulaw, u-law, μ-law | mulaw |
a-law | alaw |
The
linear_pcm family resolves to linear16, which emits a RIFF header — not to headerless pcm. If you want raw samples, ask for pcm explicitly. Prefer the canonical names in the first table; aliases exist for compatibility only.flac, ogg, aac, or an empty string — returns 422 "Unsupported audio encoding".
Choosing an encoding
- Saving a file for later playback →
mp3at defaults. - Telephony (Twilio, SIP) →
mulawat 8000 Hz. - Feeding an audio pipeline or another service →
pcm, and track the sample rate yourself. - Low-latency compressed streaming →
opus. - Something that must open in any audio editor →
wav.
Text handling
- SSML is not supported.
<speak>,<break/>and similar tags are read aloud as literal words, as are HTML entities like&. Strip markup before sending. - All input is accepted. Emoji, CJK, Arabic, Cyrillic, accented Latin, URLs, tabs, newlines and control characters never produce an error.
- Empty text succeeds, returning roughly 621 bytes of silent MP3. Validate on your side if that isn’t what you want.
Determinism
Output is non-deterministic. The sametext, voice and response_format produce different audio — and a different byte count — on every call, even at a fixed temperature.
Don’t hash or byte-diff output to test correctness. Assert on successful decode, or on duration being within an expected range, instead.
See also
- Streaming TTS — same request body, first audio in under 200 ms.
- Voices — available speaker ids.
- Errors — every status code and body shape.
- For AI agents — the whole contract on one page.
Authorizations
Body
application/json