|

Voice API

Text-to-speech and speech-to-text across OpenAI, Deepgram, Cartesia, ElevenLabs, AssemblyAI, and Spitch — one API, provider inferred from the model.

Turn text into speech and speech into text through a single billable, logged, PII-redacted gateway. Pick a model and the gateway routes to the right provider — OpenAI, Deepgram, Cartesia, ElevenLabs, AssemblyAI, or Spitch — with BYOK, spend caps, and cost tracking applied the same way as the rest of the platform.

Endpoints

PathPurpose
POST /api/ai/audio/speechText-to-speech (TTS)
POST /api/ai/audio/transcriptionsSpeech-to-text (STT)
GET /api/ai/audio/speechTTS models + formats + limits
GET /api/ai/audio/transcriptionsSTT models + supported formats + limits
GET /api/ai/audio/voicesFull TTS voice registry (runtime voice discovery)

The provider is inferred from model — you never pass a provider name. Both endpoints default to OpenAI for backward compatibility.

Text-to-Speech

const { audio } = await cencori.voice.speak({
  input: 'Hello, welcome to Cencori.',
  model: 'aura-asteria-en',   // Deepgram — provider inferred
});
 
// audio is an ArrayBuffer
await fs.writeFile('hello.mp3', Buffer.from(audio));

Parameters

ParameterTypeRequiredDescription
inputstringYesText to synthesize (max 4096 chars)
modelstringNoTTS model. Default tts-1. See the table below
voicestringNoVoice id/name; provider-specific, falls back to the model default
response_formatstringNomp3 (default), opus, aac, flac, wav, pcm (support varies by provider)
speednumberNo0.25–4.0
languagestringNoLanguage code — Spitch: en, yo, ha, ig, am
streambooleanNotrue returns chunked audio (see Streaming)

TTS models and voices

ModelProviderVoicesDefault voice
tts-1OpenAIalloy, echo, fable, onyx, nova, shimmeralloy
tts-1-hdOpenAIalloy, echo, fable, onyx, nova, shimmeralloy
aura-asteria-enDeepgram(voice baked into model)
aura-luna-enDeepgram(voice baked into model)
aura-stella-enDeepgram(voice baked into model)
aura-orion-enDeepgram(voice baked into model)
aura-arcas-enDeepgram(voice baked into model)
sonic-2Cartesia(voice is a UUID id)a0e99841-…
sonic-englishCartesia(voice is a UUID id)a0e99841-…
spitch-ttsSpitchsade, funmi, segun, femi, amara, amina, aliyu, hasan, ngozi, obinna, henry, kanisade
eleven_turbo_v2_5, eleven_flash_v2_5, eleven_multilingual_v2ElevenLabs(voice is an id)21m00Tcm4TlvDq8ikWAM

The authoritative, always-current list comes from GET /api/ai/audio/voices, which returns each model with its voices, default_voice, formats, and max_input_chars. Omit voice to get the default.

Response

POST /api/ai/audio/speech returns raw audio bytes, not JSON:

  • Content-Type: audio/mpeg (mp3), audio/opus, audio/aac, audio/flac, audio/wav, or audio/pcm per response_format
  • X-Provider: which provider served the request
  • X-Request-Id: correlation id
  • Content-Length: byte count (buffered responses)
  • X-Stream: true on streaming responses, which are chunked and have no Content-Length

Save the body directly to a file or play it — no decoding needed.

Streaming

Set "stream": true to receive the audio as a chunked Transfer-Encoding: chunked response instead of one buffered blob. The client can begin decoding/playing bytes as they arrive, and the server never holds the full file in memory.

curl -X POST https://cencori.com/api/ai/audio/speech \
  -H "CENCORI_API_KEY: csk_..." \
  -H "Content-Type: application/json" \
  -d '{ "input": "A long reply…", "stream": true }' \
  --output speech.mp3

Notes:

  • Providers still synthesize the full utterance before sending bytes (none emit partial audio today), so the win is network-level: playback starts while the file is still in transit, and no Content-Length is sent.
  • Billing, logging, and validation are identical to the buffered path; the character count is known before the first byte.
  • Errors after the first byte cannot change the HTTP status (headers are already sent) — validation errors all occur before streaming starts.

Speech-to-Text

const { text } = await cencori.voice.transcribe({
  audio: fileBytes,           // Blob, ArrayBuffer, or Uint8Array
  model: 'nova-3',            // Deepgram — provider inferred
});

Parameters

ParameterTypeRequiredDescription
filemultipart FileYesAudio to transcribe (max 25MB)
modelstringNoSTT model. Default whisper-1
languagestringNoISO language hint (Spitch: target language)
promptstringNoContext hint (OpenAI only)
temperaturenumberNo0–1 (OpenAI only)
diarizebooleanNoSpeaker labels (diarization-capable models)
response_formatstringNojson (default), text, srt, vtt, verbose_json
providerstringNoForce the provider (must match the model)

Accepted audio containers

mp3, wav, webm, mp4, m4a, ogg, flac — including browser-recorded audio/webm;codecs=opus and audio/mp4 blobs. The gateway validates by MIME type first, then falls back to the filename extension, so keep a real extension (e.g. recording.webm) on the multipart file name. Files over 25MB are rejected with 400 bad_request before any provider call.

There is no explicit audio-duration cap in the gateway; the 25MB size cap and the 55s provider timeout govern.

STT models

ModelProviderDiarizationNotes
whisper-1OpenAIGeneral purpose
nova-3DeepgramFast, cheap, word timestamps
assemblyai-universalAssemblyAIStrong long-form + speaker labels
spitch-sttSpitchAfrican languages

Response formats

response_formatBody
json (default){ "text": "…" } — flat, no wrapping
verbose_json{ "text", "language", "duration", "provider", "model", "segments", "words" } — segments/words carry optional speaker
textPlain text body, Content-Type: text/plain
srtSubRip with timestamps, Content-Type: application/x-subrip
vttWebVTT with timestamps, Content-Type: text/vtt

Every STT response includes X-Provider (and X-Request-Id).

Speaker diarization

Use diarize() (or diarize: true + response_format: 'verbose_json') with a diarization-capable model to get speaker-attributed segments:

const { segments } = await cencori.voice.diarize({
  audio: meetingBytes,
  model: 'assemblyai-universal',
});
 
for (const s of segments) {
  console.log(`${s.speaker}: ${s.text}`);
}

Subtitles (SRT / VTT)

Set response_format to srt or vtt to get ready-to-use subtitles with timestamps:

const { text: srt } = await cencori.voice.transcribe({
  audio: videoAudio,
  model: 'nova-3',
  response_format: 'srt',
});

African languages with Spitch

Spitch is the only provider here doing native Yoruba, Hausa, Igbo, and Amharic — both TTS and STT. Ideal for WhatsApp voice notes and West-African products:

// Speak Yoruba
const { audio } = await cencori.voice.speak({
  input: 'Bawo ni, e ku aaro.',
  model: 'spitch-tts',
  voice: 'sade',
  language: 'yo',
});
 
// Transcribe a Yoruba voice note
const { text } = await cencori.voice.transcribe({
  audio: voiceNote,
  model: 'spitch-stt',
  language: 'yo',
});

SDKs

The same surface exists in every SDK — TypeScript, Python, Go, PHP, and Rust.

# Python
audio = cencori.voice.speak("Hello from Cencori.", model="aura-asteria-en")
open("hello.mp3", "wb").write(audio)
 
result = cencori.voice.transcribe("hello.mp3", model="nova-3")
print(result["text"])
// Go
out, _ := c.Voice.Speak(ctx, &cencori.SpeakParams{Input: "Hello.", Model: "aura-asteria-en"})
os.WriteFile("hello.mp3", out.Audio, 0644)
 
tr, _ := c.Voice.Transcribe(ctx, out.Audio, &cencori.TranscribeParams{Model: "nova-3"})
fmt.Println(tr.Text)

Drop-in React components

import { VoiceRecorder, SpeakButton } from 'cencori/react';
 
// Record from the mic and get the transcript
<VoiceRecorder model="nova-3" apiKey={key} onTranscript={(text) => console.log(text)} />
 
// Play any text as speech
<SpeakButton text="Hello from Cencori." model="aura-asteria-en" apiKey={key} />

HTTP API

Speech (TTS)

curl -X POST https://cencori.com/api/ai/audio/speech \
  -H "CENCORI_API_KEY: csk_..." \
  -H "Content-Type: application/json" \
  -d '{
    "input": "Hello world",
    "model": "aura-asteria-en"
  }' \
  --output speech.mp3

The response carries an X-Provider header telling you which provider served the request.

Transcription (STT)

curl -X POST https://cencori.com/api/ai/audio/transcriptions \
  -H "CENCORI_API_KEY: csk_..." \
  -F file=@audio.mp3 \
  -F model=nova-3 \
  -F diarize=true \
  -F response_format=verbose_json

Voice discovery

# All TTS models with voices, defaults, and limits
curl https://cencori.com/api/ai/audio/voices
# Same registry plus formats / supports_streaming
curl https://cencori.com/api/ai/audio/speech
# STT models, accepted containers, response formats, max file size
curl https://cencori.com/api/ai/audio/transcriptions

These GET endpoints are unauthenticated and safe to call at startup.

Errors

Errors use the shared flat envelope — see the Error Reference. For these endpoints the relevant cases are:

  • 400 bad_request — unsupported model/voice/format, temperature/speed out of range, missing file, file > 25MB, input > 4096 chars
  • 400 provider_not_configured — no managed or BYOK key for the provider
  • 400 provider_error / 502 — upstream provider failed
  • 403 credit_balance_exhausted — credit-gated org out of credits (free tier is not credit-gated)
  • 429 rate_limit_exceeded — 60 requests/min per project (see Rate Limiting)

Pricing & billing

TTS bills per 1,000 characters; STT bills per minute of audio (derived from the provider's reported duration — a failed request without a duration is never billed). Every request is logged to ai_requests with the provider, model, provider cost, and your Cencori charge (provider cost + markup) — visible in the dashboard alongside chat and vision usage. BYOK keys are used when configured; otherwise the platform key is used. Streaming responses bill the same as buffered ones (characters are known before the first byte).